Compare commits
6 Commits
dev-Rene
...
762770a063
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
762770a063 | ||
|
|
3ff76228eb | ||
|
|
f01b0bcb19 | ||
|
|
4e098eefaa | ||
|
|
90f87bb538 | ||
|
|
8fe9c7ec05 |
@@ -24,6 +24,8 @@
|
|||||||
heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor
|
heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor
|
||||||
heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor
|
heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor
|
||||||
heightOverflow: { value: 0.9 }, // m, overflow elevation
|
heightOverflow: { value: 0.9 }, // m, overflow elevation
|
||||||
|
processOutputFormat: { value: "process" },
|
||||||
|
dbaseOutputFormat: { value: "influxdb" },
|
||||||
|
|
||||||
// Advanced reference information
|
// Advanced reference information
|
||||||
refHeight: { value: "NAP" }, // reference height
|
refHeight: { value: "NAP" }, // reference height
|
||||||
@@ -163,6 +165,25 @@
|
|||||||
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h3>Output Formats</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
||||||
|
<select id="node-input-processOutputFormat" style="width:60%;">
|
||||||
|
<option value="process">process</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="csv">csv</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
||||||
|
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
||||||
|
<option value="influxdb">influxdb</option>
|
||||||
|
<option value="json">json</option>
|
||||||
|
<option value="csv">csv</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Shared asset/logger/position menus -->
|
<!-- Shared asset/logger/position menus -->
|
||||||
<div id="asset-fields-placeholder"></div>
|
<div id="asset-fields-placeholder"></div>
|
||||||
<div id="logger-fields-placeholder"></div>
|
<div id="logger-fields-placeholder"></div>
|
||||||
|
|||||||
@@ -39,35 +39,20 @@ class nodeClass {
|
|||||||
const cfgMgr = new configManager();
|
const cfgMgr = new configManager();
|
||||||
this.defaultConfig = cfgMgr.getConfig(this.name);
|
this.defaultConfig = cfgMgr.getConfig(this.name);
|
||||||
|
|
||||||
// Merge UI config over defaults
|
// Build config: base sections + pumpingStation-specific domain config
|
||||||
this.config = {
|
this.config = cfgMgr.buildConfig(this.name, uiConfig, node.id, {
|
||||||
general: {
|
basin: {
|
||||||
name: this.name,
|
|
||||||
id: node.id, // node.id is for the child registration process
|
|
||||||
unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
|
|
||||||
logging: {
|
|
||||||
enabled: uiConfig.enableLog,
|
|
||||||
logLevel: uiConfig.logLevel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
functionality: {
|
|
||||||
positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified
|
|
||||||
distance: uiConfig.hasDistance ? uiConfig.distance : undefined
|
|
||||||
},
|
|
||||||
basin:{
|
|
||||||
volume: uiConfig.basinVolume,
|
volume: uiConfig.basinVolume,
|
||||||
height: uiConfig.basinHeight,
|
height: uiConfig.basinHeight,
|
||||||
heightInlet: uiConfig.heightInlet,
|
heightInlet: uiConfig.heightInlet,
|
||||||
heightOutlet: uiConfig.heightOutlet,
|
heightOutlet: uiConfig.heightOutlet,
|
||||||
heightOverflow: uiConfig.heightOverflow,
|
heightOverflow: uiConfig.heightOverflow,
|
||||||
},
|
},
|
||||||
hydraulics:{
|
hydraulics: {
|
||||||
refHeight: uiConfig.refHeight,
|
refHeight: uiConfig.refHeight,
|
||||||
basinBottomRef: uiConfig.basinBottomRef,
|
basinBottomRef: uiConfig.basinBottomRef,
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
console.log(`position vs child for ${this.name} is ${this.config.functionality.positionVsParent} the distance is ${this.config.functionality.distance}`);
|
|
||||||
|
|
||||||
// Utility for formatting outputs
|
// Utility for formatting outputs
|
||||||
this._output = new outputUtils();
|
this._output = new outputUtils();
|
||||||
@@ -203,12 +188,13 @@ class nodeClass {
|
|||||||
this.source.handleInput(msg);
|
this.source.handleInput(msg);
|
||||||
break;
|
break;
|
||||||
*/
|
*/
|
||||||
case 'registerChild':
|
case 'registerChild': {
|
||||||
// Register this node as a child of the parent node
|
// Register this node as a child of the parent node
|
||||||
const childId = msg.payload;
|
const childId = msg.payload;
|
||||||
const childObj = this.RED.nodes.getNode(childId);
|
const childObj = this.RED.nodes.getNode(childId);
|
||||||
this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
|
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation, POSITIONS} = require('generalFunctions');
|
||||||
|
|
||||||
class pumpingStation {
|
class pumpingStation {
|
||||||
constructor(config={}) {
|
constructor(config={}) {
|
||||||
@@ -41,9 +41,7 @@ class pumpingStation {
|
|||||||
//define what to do with measurements
|
//define what to do with measurements
|
||||||
if(softwareType === "measurement"){
|
if(softwareType === "measurement"){
|
||||||
const position = child.config.functionality.positionVsParent;
|
const position = child.config.functionality.positionVsParent;
|
||||||
const distance = child.config.functionality.distanceVsParent || 0;
|
|
||||||
const measurementType = child.config.asset.type;
|
const measurementType = child.config.asset.type;
|
||||||
const key = `${measurementType}_${position}`;
|
|
||||||
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
|
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
|
||||||
const eventName = `${measurementType}.measured.${position}`;
|
const eventName = `${measurementType}.measured.${position}`;
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ class pumpingStation {
|
|||||||
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
||||||
|
|
||||||
switch(child.config.functionality.positionVsParent){
|
switch(child.config.functionality.positionVsParent){
|
||||||
case("downstream"):
|
case(POSITIONS.DOWNSTREAM):
|
||||||
case("atequipment"): //in case of atequipment we also assume downstream seeing as it is registered at this pumpingstation as part of it.
|
case("atequipment"): //in case of atequipment we also assume downstream seeing as it is registered at this pumpingstation as part of it.
|
||||||
//for now lets focus on handling downstream predicted flow
|
//for now lets focus on handling downstream predicted flow
|
||||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||||
@@ -80,7 +78,7 @@ class pumpingStation {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case("upstream"):
|
case(POSITIONS.UPSTREAM):
|
||||||
//check for predicted outgoing flow at the connected child pumpingsation
|
//check for predicted outgoing flow at the connected child pumpingsation
|
||||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||||
@@ -97,7 +95,7 @@ class pumpingStation {
|
|||||||
|
|
||||||
// add one for group later
|
// add one for group later
|
||||||
if( softwareType == "machineGroup" ){
|
if( softwareType == "machineGroup" ){
|
||||||
|
/* intentionally empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
// add one for pumping station
|
// add one for pumping station
|
||||||
@@ -109,7 +107,7 @@ class pumpingStation {
|
|||||||
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
||||||
|
|
||||||
switch(child.config.functionality.positionVsParent){
|
switch(child.config.functionality.positionVsParent){
|
||||||
case("downstream"):
|
case(POSITIONS.DOWNSTREAM):
|
||||||
//check for predicted outgoing flow at the connected child pumpingsation
|
//check for predicted outgoing flow at the connected child pumpingsation
|
||||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||||
@@ -118,7 +116,7 @@ class pumpingStation {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case("upstream"):
|
case(POSITIONS.UPSTREAM):
|
||||||
//check for predicted outgoing flow at the connected child pumpingsation
|
//check for predicted outgoing flow at the connected child pumpingsation
|
||||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||||
@@ -139,7 +137,7 @@ class pumpingStation {
|
|||||||
|
|
||||||
//get downflow
|
//get downflow
|
||||||
const seriesExists = this.measurements.type("flow").variant("predicted").position(flowDir).exists();
|
const seriesExists = this.measurements.type("flow").variant("predicted").position(flowDir).exists();
|
||||||
if(!seriesExists){return};
|
if(!seriesExists){return}
|
||||||
|
|
||||||
const series = this.measurements.type("flow").variant("predicted").position(flowDir);
|
const series = this.measurements.type("flow").variant("predicted").position(flowDir);
|
||||||
const currFLow = series.getLaggedValue(0, "m3/s"); // { value, timestamp, unit }
|
const currFLow = series.getLaggedValue(0, "m3/s"); // { value, timestamp, unit }
|
||||||
@@ -162,7 +160,7 @@ class pumpingStation {
|
|||||||
const calcVol = avgFlow * deltaSeconds;
|
const calcVol = avgFlow * deltaSeconds;
|
||||||
|
|
||||||
//substract seeing as this is downstream and is being pulled away from the pumpingstaion and keep track of status
|
//substract seeing as this is downstream and is being pulled away from the pumpingstaion and keep track of status
|
||||||
const currVolume = this.measurements.type('volume').variant('predicted').position('atEquipment').getCurrentValue('m3');
|
const currVolume = this.measurements.type('volume').variant('predicted').position(POSITIONS.AT_EQUIPMENT).getCurrentValue('m3');
|
||||||
let newVol = currVolume;
|
let newVol = currVolume;
|
||||||
|
|
||||||
switch(flowDir){
|
switch(flowDir){
|
||||||
@@ -179,11 +177,11 @@ class pumpingStation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.measurements.type('volume').variant('predicted').position('atEquipment').value(newVol).unit('m3');
|
this.measurements.type('volume').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(newVol).unit('m3');
|
||||||
//convert to a predicted level
|
//convert to a predicted level
|
||||||
const newLevel = this._calcLevelFromVolume(newVol);
|
const newLevel = this._calcLevelFromVolume(newVol);
|
||||||
|
|
||||||
this.measurements.type('level').variant('predicted').position('atEquipment').value(newLevel).unit('m');
|
this.measurements.type('level').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(newLevel).unit('m');
|
||||||
|
|
||||||
this.logger.debug(`new predicted volume : ${newVol} new predicted level: ${newLevel} `);
|
this.logger.debug(`new predicted volume : ${newVol} new predicted level: ${newLevel} `);
|
||||||
|
|
||||||
@@ -213,7 +211,7 @@ class pumpingStation {
|
|||||||
this._updateVolumePrediction("in"); // check for changes in incomming flow
|
this._updateVolumePrediction("in"); // check for changes in incomming flow
|
||||||
//calc the most important values back to determine state and net up or downstream flow
|
//calc the most important values back to determine state and net up or downstream flow
|
||||||
this._calcNetFlow();
|
this._calcNetFlow();
|
||||||
this._calcTimeRemaining();
|
this._calcRemainingTime();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,13 +255,13 @@ class pumpingStation {
|
|||||||
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
|
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
|
||||||
|
|
||||||
//convert pressure to level based on density of water and height of pressure sensor
|
//convert pressure to level based on density of water and height of pressure sensor
|
||||||
const mTemp = this.measurements.type("temperature").variant("measured").position("atEquipment").getCurrentValue('K'); //default to 20C if no temperature measurement
|
const mTemp = this.measurements.type("temperature").variant("measured").position(POSITIONS.AT_EQUIPMENT).getCurrentValue('K'); //default to 20C if no temperature measurement
|
||||||
|
|
||||||
//prefer measured temp but otherwise assume nominal temp for wastewater
|
//prefer measured temp but otherwise assume nominal temp for wastewater
|
||||||
if(mTemp === null){
|
if(mTemp === null){
|
||||||
this.logger.warn(`No temperature measurement available, defaulting to 15C for pressure to level conversion.`);
|
this.logger.warn(`No temperature measurement available, defaulting to 15C for pressure to level conversion.`);
|
||||||
this.measurements.type("temperature").variant("assumed").position("atEquipment").value(15, Date.now(), "C");
|
this.measurements.type("temperature").variant("assumed").position(POSITIONS.AT_EQUIPMENT).value(15, Date.now(), "C");
|
||||||
kelvinTemp = this.measurements.type('temperature').variant('assumed').position('atEquipment').getCurrentValue('K');
|
kelvinTemp = this.measurements.type('temperature').variant('assumed').position(POSITIONS.AT_EQUIPMENT).getCurrentValue('K');
|
||||||
this.logger.debug(`Temperature is : ${kelvinTemp}`);
|
this.logger.debug(`Temperature is : ${kelvinTemp}`);
|
||||||
} else {
|
} else {
|
||||||
kelvinTemp = mTemp;
|
kelvinTemp = mTemp;
|
||||||
@@ -294,15 +292,14 @@ class pumpingStation {
|
|||||||
|
|
||||||
const proc = this.interpolate.interpolate_lin_single_point(volume,this.basin.minVol,this.basin.maxVolOverflow,0,100);
|
const proc = this.interpolate.interpolate_lin_single_point(volume,this.basin.minVol,this.basin.maxVolOverflow,0,100);
|
||||||
this.logger.debug(`PROC volume : ${proc}`);
|
this.logger.debug(`PROC volume : ${proc}`);
|
||||||
this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3');
|
this.measurements.type("volume").variant("measured").position(POSITIONS.AT_EQUIPMENT).value(volume).unit('m3');
|
||||||
this.measurements.type("volume").variant("procent").position("atEquipment").value(proc);
|
this.measurements.type("volume").variant("procent").position(POSITIONS.AT_EQUIPMENT).value(proc);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_calcNetFlow() {
|
_calcNetFlow() {
|
||||||
let netFlow = null;
|
|
||||||
|
|
||||||
const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: "downstream", to: "upstream", unit: "m3/s" }));
|
const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: POSITIONS.DOWNSTREAM, to: POSITIONS.UPSTREAM, unit: "m3/s" }));
|
||||||
const netFlow_LevelSensor = this._calcNetFlowFromLevelDiff();
|
const netFlow_LevelSensor = this._calcNetFlowFromLevelDiff();
|
||||||
const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" }));
|
const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" }));
|
||||||
|
|
||||||
@@ -325,8 +322,9 @@ class pumpingStation {
|
|||||||
_calcRemainingTime(level,variant){
|
_calcRemainingTime(level,variant){
|
||||||
|
|
||||||
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
||||||
const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: "downstream", to: "upstream", unit: "m3/s" });
|
const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: POSITIONS.DOWNSTREAM, to: POSITIONS.UPSTREAM, unit: "m3/s" });
|
||||||
|
|
||||||
|
let remainingHeight;
|
||||||
switch(true){
|
switch(true){
|
||||||
case(flowDiff>0):
|
case(flowDiff>0):
|
||||||
remainingHeight = Math.max(heightOverflow - level, 0);
|
remainingHeight = Math.max(heightOverflow - level, 0);
|
||||||
@@ -348,6 +346,7 @@ class pumpingStation {
|
|||||||
_calcDirection(flowDiff){
|
_calcDirection(flowDiff){
|
||||||
|
|
||||||
let direction = null;
|
let direction = null;
|
||||||
|
const flowThreshold = 0.001;
|
||||||
|
|
||||||
switch (true){
|
switch (true){
|
||||||
case flowDiff > flowThreshold:
|
case flowDiff > flowThreshold:
|
||||||
@@ -372,7 +371,7 @@ class pumpingStation {
|
|||||||
|
|
||||||
_calcNetFlowFromLevelDiff() {
|
_calcNetFlowFromLevelDiff() {
|
||||||
const { surfaceArea } = this.basin;
|
const { surfaceArea } = this.basin;
|
||||||
const levelObj = this.measurements.type("level").variant("measured").position("atEquipment");
|
const levelObj = this.measurements.type("level").variant("measured").position(POSITIONS.AT_EQUIPMENT);
|
||||||
const level = levelObj.getCurrentValue("m");
|
const level = levelObj.getCurrentValue("m");
|
||||||
const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit }
|
const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit }
|
||||||
const measurement = levelObj.get();
|
const measurement = levelObj.get();
|
||||||
@@ -424,7 +423,7 @@ class pumpingStation {
|
|||||||
this.basin.minVolOut = minVolOut ;
|
this.basin.minVolOut = minVolOut ;
|
||||||
|
|
||||||
//init predicted min volume to min vol in order to have a starting point
|
//init predicted min volume to min vol in order to have a starting point
|
||||||
this.measurements.type("volume").variant("predicted").position("atEquipment").value(minVol).unit('m3');
|
this.measurements.type("volume").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(minVol).unit('m3');
|
||||||
|
|
||||||
this.logger.debug(`
|
this.logger.debug(`
|
||||||
Basin initialized | area=${surfaceArea.toFixed(2)} m²,
|
Basin initialized | area=${surfaceArea.toFixed(2)} m²,
|
||||||
@@ -522,7 +521,7 @@ function createLevelMeasurementConfig(name) {
|
|||||||
functionality: {
|
functionality: {
|
||||||
softwareType: "measurement",
|
softwareType: "measurement",
|
||||||
role: "sensor",
|
role: "sensor",
|
||||||
positionVsParent: "atEquipment"
|
positionVsParent: POSITIONS.AT_EQUIPMENT
|
||||||
},
|
},
|
||||||
asset: {
|
asset: {
|
||||||
category: "sensor",
|
category: "sensor",
|
||||||
@@ -564,7 +563,6 @@ function createFlowMeasurementConfig(name, position) {
|
|||||||
|
|
||||||
function createMachineConfig(name) {
|
function createMachineConfig(name) {
|
||||||
|
|
||||||
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
general: {
|
general: {
|
||||||
@@ -605,7 +603,7 @@ function createMachineStateConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convenience for seeding measurements
|
// convenience for seeding measurements
|
||||||
function pushSample(measurement, type, value, unit) {
|
function pushSample(measurement, type, value, unit) { // eslint-disable-line no-unused-vars
|
||||||
const pos = measurement.config.functionality.positionVsParent;
|
const pos = measurement.config.functionality.positionVsParent;
|
||||||
measurement.measurements
|
measurement.measurements
|
||||||
.type(type)
|
.type(type)
|
||||||
@@ -615,13 +613,14 @@ function pushSample(measurement, type, value, unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Demo *********************************************************************/
|
/** Demo *********************************************************************/
|
||||||
(async function demoStationWithPump() {
|
// Only run the demo when this file is executed directly (not when required as a module)
|
||||||
|
if (require.main === module) (async function demoStationWithPump() {
|
||||||
const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo"));
|
const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo"));
|
||||||
const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
|
const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
|
||||||
|
|
||||||
const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel"));
|
const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel")); // eslint-disable-line no-unused-vars
|
||||||
const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream"));
|
const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", POSITIONS.UPSTREAM)); // eslint-disable-line no-unused-vars
|
||||||
const downstreamFlow = new Measurement(createFlowMeasurementConfig("PumpDischargeFlow", "downstream"));
|
const downstreamFlow = new Measurement(createFlowMeasurementConfig("PumpDischargeFlow", POSITIONS.DOWNSTREAM));
|
||||||
|
|
||||||
|
|
||||||
// station uses the sensors
|
// station uses the sensors
|
||||||
@@ -633,7 +632,7 @@ function pushSample(measurement, type, value, unit) {
|
|||||||
|
|
||||||
// pump owns the downstream flow sensor
|
// pump owns the downstream flow sensor
|
||||||
pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
||||||
station.childRegistrationUtils.registerChild(pump,"downstream");
|
station.childRegistrationUtils.registerChild(pump, POSITIONS.DOWNSTREAM);
|
||||||
|
|
||||||
setInterval(() => station.tick(), 1000);
|
setInterval(() => station.tick(), 1000);
|
||||||
|
|
||||||
|
|||||||
260
test/specificClass.test.js
Normal file
260
test/specificClass.test.js
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* Tests for pumpingStation specificClass (domain logic).
|
||||||
|
*
|
||||||
|
* The pumpingStation class manages a basin (wet well):
|
||||||
|
* - initBasinProperties: derives surface area, volumes from config
|
||||||
|
* - _calcVolumeFromLevel / _calcLevelFromVolume: linear geometry
|
||||||
|
* - _calcDirection: filling / draining / stable from flow diff
|
||||||
|
* - _callMeasurementHandler: dispatches to type-specific handlers
|
||||||
|
* - getOutput: builds an output snapshot
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PumpingStation = require('../src/specificClass');
|
||||||
|
|
||||||
|
// --------------- helpers ---------------
|
||||||
|
|
||||||
|
function makeConfig(overrides = {}) {
|
||||||
|
const base = {
|
||||||
|
general: {
|
||||||
|
name: 'TestStation',
|
||||||
|
id: 'ps-test-1',
|
||||||
|
unit: 'm3/h',
|
||||||
|
logging: { enabled: false, logLevel: 'error' },
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
softwareType: 'pumpingStation',
|
||||||
|
role: 'stationcontroller',
|
||||||
|
positionVsParent: 'atEquipment',
|
||||||
|
},
|
||||||
|
basin: {
|
||||||
|
volume: 50, // m3 (empty basin volume)
|
||||||
|
height: 5, // m
|
||||||
|
heightInlet: 0.3, // m
|
||||||
|
heightOutlet: 0.2, // m
|
||||||
|
heightOverflow: 4.0, // m
|
||||||
|
},
|
||||||
|
hydraulics: {
|
||||||
|
refHeight: 'NAP',
|
||||||
|
basinBottomRef: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(overrides)) {
|
||||||
|
if (typeof overrides[key] === 'object' && !Array.isArray(overrides[key]) && base[key]) {
|
||||||
|
base[key] = { ...base[key], ...overrides[key] };
|
||||||
|
} else {
|
||||||
|
base[key] = overrides[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------- tests ---------------
|
||||||
|
|
||||||
|
describe('pumpingStation specificClass', () => {
|
||||||
|
|
||||||
|
describe('constructor / initialization', () => {
|
||||||
|
it('should create an instance with the given config', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
expect(ps).toBeDefined();
|
||||||
|
expect(ps.config.general.name).toBe('teststation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize state object with default values', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
expect(ps.state).toEqual({ direction: '', netDownstream: 0, netUpstream: 0, seconds: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize empty machines, stations, child, parent objects', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
expect(ps.machines).toEqual({});
|
||||||
|
expect(ps.stations).toEqual({});
|
||||||
|
expect(ps.child).toEqual({});
|
||||||
|
expect(ps.parent).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initBasinProperties()', () => {
|
||||||
|
it('should calculate surfaceArea = volume / height', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// 50 / 5 = 10 m2
|
||||||
|
expect(ps.basin.surfaceArea).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate maxVol = height * surfaceArea', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// 5 * 10 = 50
|
||||||
|
expect(ps.basin.maxVol).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate maxVolOverflow = heightOverflow * surfaceArea', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// 4.0 * 10 = 40
|
||||||
|
expect(ps.basin.maxVolOverflow).toBe(40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate minVol = heightOutlet * surfaceArea', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// 0.2 * 10 = 2
|
||||||
|
expect(ps.basin.minVol).toBeCloseTo(2, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate minVolOut = heightInlet * surfaceArea', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// 0.3 * 10 = 3
|
||||||
|
expect(ps.basin.minVolOut).toBeCloseTo(3, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store the raw config values on basin', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
expect(ps.basin.volEmptyBasin).toBe(50);
|
||||||
|
expect(ps.basin.heightBasin).toBe(5);
|
||||||
|
expect(ps.basin.heightInlet).toBe(0.3);
|
||||||
|
expect(ps.basin.heightOutlet).toBe(0.2);
|
||||||
|
expect(ps.basin.heightOverflow).toBe(4.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_calcVolumeFromLevel()', () => {
|
||||||
|
let ps;
|
||||||
|
beforeAll(() => { ps = new PumpingStation(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return level * surfaceArea', () => {
|
||||||
|
// surfaceArea = 10, level = 2 => 20
|
||||||
|
expect(ps._calcVolumeFromLevel(2)).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 for level = 0', () => {
|
||||||
|
expect(ps._calcVolumeFromLevel(0)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp negative levels to 0', () => {
|
||||||
|
expect(ps._calcVolumeFromLevel(-3)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_calcLevelFromVolume()', () => {
|
||||||
|
let ps;
|
||||||
|
beforeAll(() => { ps = new PumpingStation(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return volume / surfaceArea', () => {
|
||||||
|
// surfaceArea = 10, vol = 20 => 2
|
||||||
|
expect(ps._calcLevelFromVolume(20)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 for volume = 0', () => {
|
||||||
|
expect(ps._calcLevelFromVolume(0)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp negative volumes to 0', () => {
|
||||||
|
expect(ps._calcLevelFromVolume(-10)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('volume/level roundtrip', () => {
|
||||||
|
it('should roundtrip level -> volume -> level', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
const level = 2.7;
|
||||||
|
const vol = ps._calcVolumeFromLevel(level);
|
||||||
|
const levelBack = ps._calcLevelFromVolume(vol);
|
||||||
|
expect(levelBack).toBeCloseTo(level, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_calcDirection()', () => {
|
||||||
|
let ps;
|
||||||
|
beforeAll(() => { ps = new PumpingStation(makeConfig()); });
|
||||||
|
|
||||||
|
it('should return "filling" for positive flow above threshold', () => {
|
||||||
|
expect(ps._calcDirection(0.01)).toBe('filling');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "draining" for negative flow below negative threshold', () => {
|
||||||
|
expect(ps._calcDirection(-0.01)).toBe('draining');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "stable" for flow near zero (within threshold)', () => {
|
||||||
|
expect(ps._calcDirection(0.0005)).toBe('stable');
|
||||||
|
expect(ps._calcDirection(-0.0005)).toBe('stable');
|
||||||
|
expect(ps._calcDirection(0)).toBe('stable');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_callMeasurementHandler()', () => {
|
||||||
|
it('should not throw for flow and temperature measurement types', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// flow and temperature handlers are empty stubs, safe to call
|
||||||
|
expect(() => ps._callMeasurementHandler('flow', 0.5, 'downstream', {})).not.toThrow();
|
||||||
|
expect(() => ps._callMeasurementHandler('temperature', 15, 'atEquipment', {})).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch to the correct handler based on measurement type', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// Verify the switch dispatches by checking it does not warn for known types
|
||||||
|
// pressure handler stores values and attempts coolprop calculation
|
||||||
|
// level handler stores values and computes volume
|
||||||
|
// We verify the dispatch logic by calling with type and checking no unhandled error
|
||||||
|
const spy = jest.spyOn(ps, 'updateMeasuredFlow');
|
||||||
|
ps._callMeasurementHandler('flow', 0.5, 'downstream', {});
|
||||||
|
expect(spy).toHaveBeenCalledWith(0.5, 'downstream', {});
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getOutput()', () => {
|
||||||
|
it('should return an object containing state and basin', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
const out = ps.getOutput();
|
||||||
|
expect(out).toHaveProperty('state');
|
||||||
|
expect(out).toHaveProperty('basin');
|
||||||
|
expect(out.state).toBe(ps.state);
|
||||||
|
expect(out.basin).toBe(ps.basin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include measurement keys in the output', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
const out = ps.getOutput();
|
||||||
|
// After initialization the predicted volume is set
|
||||||
|
expect(typeof out).toBe('object');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_calcRemainingTime()', () => {
|
||||||
|
it('should not throw when called with a level and variant', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
// Should not throw even with no measurement data; it will just find null diffs
|
||||||
|
expect(() => ps._calcRemainingTime(2, 'predicted')).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tick()', () => {
|
||||||
|
it('should call _updateVolumePrediction and _calcNetFlow', () => {
|
||||||
|
const ps = new PumpingStation(makeConfig());
|
||||||
|
const spyVol = jest.spyOn(ps, '_updateVolumePrediction');
|
||||||
|
const spyNet = jest.spyOn(ps, '_calcNetFlow');
|
||||||
|
// stub _calcRemainingTime to avoid needing full measurement data
|
||||||
|
ps._calcRemainingTime = jest.fn();
|
||||||
|
ps.tick();
|
||||||
|
expect(spyVol).toHaveBeenCalledWith('out');
|
||||||
|
expect(spyVol).toHaveBeenCalledWith('in');
|
||||||
|
expect(spyNet).toHaveBeenCalled();
|
||||||
|
spyVol.mockRestore();
|
||||||
|
spyNet.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle basin with zero height gracefully', () => {
|
||||||
|
// surfaceArea = volume / height => division by 0 gives Infinity
|
||||||
|
const config = makeConfig({ basin: { volume: 50, height: 0, heightInlet: 0, heightOutlet: 0, heightOverflow: 0 } });
|
||||||
|
const ps = new PumpingStation(config);
|
||||||
|
expect(ps.basin.surfaceArea).toBe(Infinity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle basin with very small dimensions', () => {
|
||||||
|
const config = makeConfig({ basin: { volume: 0.001, height: 0.001, heightInlet: 0, heightOutlet: 0, heightOverflow: 0.0005 } });
|
||||||
|
const ps = new PumpingStation(config);
|
||||||
|
expect(ps.basin.surfaceArea).toBeCloseTo(1, 5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user