Compare commits
13 Commits
1848486f1c
...
dev-Rene
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7efd3b0a07 | ||
|
|
c81ee1b470 | ||
|
|
955c17a466 | ||
|
|
052ded7b6e | ||
|
|
321ea33bf7 | ||
|
|
288bd244dd | ||
|
|
d91609b3a4 | ||
|
|
5a575a29fe | ||
|
|
0a6c7ee2e1 | ||
|
|
4cc529b1c2 | ||
|
|
fbfcec4b47 | ||
|
|
43eb97407f | ||
|
|
9e4b149b64 |
@@ -24,6 +24,12 @@
|
|||||||
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
|
||||||
|
timeleftToFullOrEmptyThresholdSeconds:{value:0}, // time threshold to safeguard starting or stopping pumps in seconds
|
||||||
|
enableDryRunProtection: { value: true },
|
||||||
|
enableOverfillProtection: { value: true },
|
||||||
|
dryRunThresholdPercent: { value: 2 },
|
||||||
|
overfillThresholdPercent: { value: 98 },
|
||||||
|
minHeightBasedOn: { value: "outlet" }, // basis for minimum height check: inlet or outlet
|
||||||
|
|
||||||
// Advanced reference information
|
// Advanced reference information
|
||||||
refHeight: { value: "NAP" }, // reference height
|
refHeight: { value: "NAP" }, // reference height
|
||||||
@@ -47,7 +53,16 @@
|
|||||||
hasDistance: { value: false },
|
hasDistance: { value: false },
|
||||||
distance: { value: 0 },
|
distance: { value: 0 },
|
||||||
distanceUnit: { value: "m" },
|
distanceUnit: { value: "m" },
|
||||||
distanceDescription: { value: "" }
|
distanceDescription: { value: "" },
|
||||||
|
|
||||||
|
// control strategy
|
||||||
|
controlMode: { value: "none" },
|
||||||
|
startLevel: { value: null },
|
||||||
|
stopLevel: { value: null },
|
||||||
|
minFlowLevel: { value: null },
|
||||||
|
maxFlowLevel: { value: null },
|
||||||
|
flowSetpoint: { value: null },
|
||||||
|
flowDeadband: { value: null }
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -86,6 +101,68 @@
|
|||||||
refHeightEl.value = this.refHeight || "NAP";
|
refHeightEl.value = this.refHeight || "NAP";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minHeightBasedOnEl = document.getElementById("node-input-minHeightBasedOn");
|
||||||
|
if (minHeightBasedOnEl) {
|
||||||
|
minHeightBasedOnEl.value = this.minHeightBasedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dryRunToggle = document.getElementById("node-input-enableDryRunProtection");
|
||||||
|
const dryRunPercent = document.getElementById("node-input-dryRunThresholdPercent");
|
||||||
|
const overfillToggle = document.getElementById("node-input-enableOverfillProtection");
|
||||||
|
const overfillPercent = document.getElementById("node-input-overfillThresholdPercent");
|
||||||
|
|
||||||
|
const toggleInput = (toggleEl, inputEl) => {
|
||||||
|
if (!toggleEl || !inputEl) { return; }
|
||||||
|
inputEl.disabled = !toggleEl.checked;
|
||||||
|
inputEl.parentElement.classList.toggle('disabled', inputEl.disabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dryRunToggle && dryRunPercent) {
|
||||||
|
dryRunToggle.checked = !!this.enableDryRunProtection;
|
||||||
|
dryRunPercent.value = Number.isFinite(this.dryRunThresholdPercent) ? this.dryRunThresholdPercent : 2;
|
||||||
|
dryRunToggle.addEventListener('change', () => toggleInput(dryRunToggle, dryRunPercent));
|
||||||
|
toggleInput(dryRunToggle, dryRunPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overfillToggle && overfillPercent) {
|
||||||
|
overfillToggle.checked = !!this.enableOverfillProtection;
|
||||||
|
overfillPercent.value = Number.isFinite(this.overfillThresholdPercent) ? this.overfillThresholdPercent : 98;
|
||||||
|
overfillToggle.addEventListener('change', () => toggleInput(overfillToggle, overfillPercent));
|
||||||
|
toggleInput(overfillToggle, overfillPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLeftInput = document.getElementById("node-input-timeleftToFullOrEmptyThresholdSeconds");
|
||||||
|
if (timeLeftInput) {
|
||||||
|
timeLeftInput.value = Number.isFinite(this.timeleftToFullOrEmptyThresholdSeconds)
|
||||||
|
? this.timeleftToFullOrEmptyThresholdSeconds
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// control mode toggle UI
|
||||||
|
const toggleModeSections = (val) => {
|
||||||
|
document.querySelectorAll('.ps-mode-section').forEach((el) => el.style.display = 'none');
|
||||||
|
const active = document.getElementById(`ps-mode-${val}`);
|
||||||
|
if (active) active.style.display = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const modeSelect = document.getElementById('node-input-controlMode');
|
||||||
|
if (modeSelect) {
|
||||||
|
modeSelect.value = this.controlMode || 'none';
|
||||||
|
toggleModeSections(modeSelect.value);
|
||||||
|
modeSelect.addEventListener('change', (e) => toggleModeSections(e.target.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
const setNumberField = (id, val) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = Number.isFinite(val) ? val : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
setNumberField('node-input-startLevel', this.startLevel);
|
||||||
|
setNumberField('node-input-stopLevel', this.stopLevel);
|
||||||
|
setNumberField('node-input-minFlowLevel', this.minFlowLevel);
|
||||||
|
setNumberField('node-input-maxFlowLevel', this.maxFlowLevel);
|
||||||
|
setNumberField('node-input-flowSetpoint', this.flowSetpoint);
|
||||||
|
setNumberField('node-input-flowDeadband', this.flowDeadband);
|
||||||
|
|
||||||
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
|
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
|
||||||
},
|
},
|
||||||
@@ -98,14 +175,29 @@
|
|||||||
|
|
||||||
//node specific
|
//node specific
|
||||||
node.refHeight = document.getElementById("node-input-refHeight").value || "NAP";
|
node.refHeight = document.getElementById("node-input-refHeight").value || "NAP";
|
||||||
|
node.minHeightBasedOn = document.getElementById("node-input-minHeightBasedOn").value || "outlet";
|
||||||
node.simulator = document.getElementById("node-input-simulator").checked;
|
node.simulator = document.getElementById("node-input-simulator").checked;
|
||||||
|
|
||||||
["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef"]
|
["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef","timeleftToFullOrEmptyThresholdSeconds","dryRunThresholdPercent","overfillThresholdPercent"]
|
||||||
.forEach(field => {
|
.forEach(field => {
|
||||||
node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0;
|
node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
node.refHeight = document.getElementById("node-input-refHeight").value || "";
|
node.refHeight = document.getElementById("node-input-refHeight").value || "";
|
||||||
|
node.enableDryRunProtection = document.getElementById("node-input-enableDryRunProtection").checked;
|
||||||
|
node.enableOverfillProtection = document.getElementById("node-input-enableOverfillProtection").checked;
|
||||||
|
|
||||||
|
// control strategy
|
||||||
|
node.controlMode = document.getElementById('node-input-controlMode').value || 'none';
|
||||||
|
|
||||||
|
const parseNum = (id) => parseFloat(document.getElementById(id)?.value);
|
||||||
|
node.startLevel = parseNum('node-input-startLevel');
|
||||||
|
node.stopLevel = parseNum('node-input-stopLevel');
|
||||||
|
node.minFlowLevel = parseNum('node-input-minFlowLevel');
|
||||||
|
node.maxFlowLevel = parseNum('node-input-maxFlowLevel');
|
||||||
|
node.flowSetpoint = parseNum('node-input-flowSetpoint');
|
||||||
|
node.flowDeadband = parseNum('node-input-flowDeadband');
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -115,7 +207,7 @@
|
|||||||
|
|
||||||
<script type="text/html" data-template-name="pumpingStation">
|
<script type="text/html" data-template-name="pumpingStation">
|
||||||
|
|
||||||
<!-- Simulator toggle -->
|
<h4>Simulation</h4>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-simulator"><i class="fa fa-play-circle"></i> Simulator</label>
|
<label for="node-input-simulator"><i class="fa fa-play-circle"></i> Simulator</label>
|
||||||
<input type="checkbox" id="node-input-simulator" style="width:20px;vertical-align:baseline;" />
|
<input type="checkbox" id="node-input-simulator" style="width:20px;vertical-align:baseline;" />
|
||||||
@@ -123,8 +215,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- Basin geometry -->
|
<h4>Basin Geometry</h4>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label>
|
<label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label>
|
||||||
<input type="number" id="node-input-basinVolume" min="0" step="0.1" />
|
<input type="number" id="node-input-basinVolume" min="0" step="0.1" />
|
||||||
@@ -150,7 +242,58 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<h4>Control Strategy</h4>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-controlMode"><i class="fa fa-sliders"></i> Control mode</label>
|
||||||
|
<select id="node-input-controlMode">
|
||||||
|
<option value="none">None / Manual</option>
|
||||||
|
<option value="levelbased">Level-based</option>
|
||||||
|
<option value="flowbased">Flow-based</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ps-mode-levelbased" class="ps-mode-section">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-startLevel">startLevel</label>
|
||||||
|
<input type="number" id="node-input-startLevel" placeholder="m" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-stopLevel">stopLevel</label>
|
||||||
|
<input type="number" id="node-input-stopLevel" placeholder="m" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-minFlowLevel">Min flow (m)</label>
|
||||||
|
<input type="number" id="node-input-minFlowLevel" placeholder="m" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-maxFlowLevel">Max flow (m)</label>
|
||||||
|
<input type="number" id="node-input-maxFlowLevel" placeholder="m" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ps-mode-flowbased" class="ps-mode-section" style="display:none">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-flowSetpoint">Flow setpoint</label>
|
||||||
|
<input type="number" id="node-input-flowSetpoint" placeholder="m3/h" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-flowDeadband">Deadband</label>
|
||||||
|
<input type="number" id="node-input-flowDeadband" placeholder="m3/h" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>Reference</h4>
|
||||||
|
|
||||||
<!-- Reference data -->
|
<!-- Reference data -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-minHeightBasedOn"><i class="fa fa-arrows-v"></i> Minimum Height Based On</label>
|
||||||
|
<select id="node-input-minHeightBasedOn" style="width:60%;">
|
||||||
|
<option value="inlet">Inlet Elevation</option>
|
||||||
|
<option value="outlet">Outlet Elevation</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
|
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
|
||||||
<select id="node-input-refHeight" style="width:60%;">
|
<select id="node-input-refHeight" style="width:60%;">
|
||||||
@@ -163,6 +306,40 @@
|
|||||||
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>Safety</h4>
|
||||||
|
|
||||||
|
<!-- Safety settings -->
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-timeleftToFullOrEmptyThresholdSeconds"><i class="fa fa-clock-o"></i> Time To Empty/Full (s)</label>
|
||||||
|
<input type="number" id="node-input-timeleftToFullOrEmptyThresholdSeconds" min="0" step="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-enableDryRunProtection">
|
||||||
|
<i class="fa fa-shield"></i> Dry-run Protection
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" id="node-input-enableDryRunProtection" style="width:20px;vertical-align:baseline;" />
|
||||||
|
<span>Prevent pumps from running on low volume</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-dryRunThresholdPercent" style="padding-left:20px;">Low Volume Threshold (%)</label>
|
||||||
|
<input type="number" id="node-input-dryRunThresholdPercent" min="0" max="100" step="0.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-enableOverfillProtection">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i> Overfill Protection
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" id="node-input-enableOverfillProtection" style="width:20px;vertical-align:baseline;" />
|
||||||
|
<span>Stop filling when approaching overflow</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-overfillThresholdPercent" style="padding-left:20px;">High Volume Threshold (%)</label>
|
||||||
|
<input type="number" id="node-input-overfillThresholdPercent" min="0" max="100" step="0.1" />
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
<!-- 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>
|
||||||
|
|||||||
@@ -63,7 +63,24 @@ class nodeClass {
|
|||||||
},
|
},
|
||||||
hydraulics:{
|
hydraulics:{
|
||||||
refHeight: uiConfig.refHeight,
|
refHeight: uiConfig.refHeight,
|
||||||
|
minHeightBasedOn: uiConfig.minHeightBasedOn,
|
||||||
basinBottomRef: uiConfig.basinBottomRef,
|
basinBottomRef: uiConfig.basinBottomRef,
|
||||||
|
},
|
||||||
|
control:{
|
||||||
|
mode: uiConfig.controlMode,
|
||||||
|
levelbased:{
|
||||||
|
startLevel:uiConfig.startLevel,
|
||||||
|
stopLevel:uiConfig.stopLevel,
|
||||||
|
minFlowLevel:uiConfig.minFlowLevel,
|
||||||
|
maxFlowLevel:uiConfig.maxFlowLevel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
safety:{
|
||||||
|
enableDryRunProtection: uiConfig.enableDryRunProtection,
|
||||||
|
dryRunThresholdPercent: uiConfig.dryRunThresholdPercent,
|
||||||
|
enableOverfillProtection: uiConfig.enableOverfillProtection,
|
||||||
|
overfillThresholdPercent: uiConfig.overfillThresholdPercent,
|
||||||
|
timeleftToFullOrEmptyThresholdSeconds: uiConfig.timeleftToFullOrEmptyThresholdSeconds
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,19 +208,33 @@ class nodeClass {
|
|||||||
this.node.on('input', (msg, send, done) => {
|
this.node.on('input', (msg, send, done) => {
|
||||||
switch (msg.topic) {
|
switch (msg.topic) {
|
||||||
//example
|
//example
|
||||||
/*case 'simulator':
|
case 'changemode':
|
||||||
this.source.toggleSimulation();
|
this.source.changeMode(msg.payload);
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
this.source.handleInput(msg);
|
|
||||||
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;
|
||||||
|
case 'calibratePredictedVolume':
|
||||||
|
const injectedVol = parseFloat(msg.payload);
|
||||||
|
this.source.calibratePredictedVolume(injectedVol);
|
||||||
|
break;
|
||||||
|
case 'calibratePredictedLevel':
|
||||||
|
const injectedLevel = parseFloat(msg.payload);
|
||||||
|
this.source.calibratePredictedLevel(injectedLevel);
|
||||||
|
break;
|
||||||
|
case 'q_in': {
|
||||||
|
// payload can be number or { value, unit, timestamp }
|
||||||
|
const val = Number(msg.payload);
|
||||||
|
const unit = msg?.unit;
|
||||||
|
const ts = msg?.timestamp || Date.now();
|
||||||
|
this.source.setManualInflow(val, ts, unit);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
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
|
||||||
|
} = require('generalFunctions');
|
||||||
|
|
||||||
class PumpingStation {
|
class PumpingStation {
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
this.emitter = new EventEmitter();
|
this.emitter = new EventEmitter();
|
||||||
@@ -10,37 +19,35 @@ class PumpingStation {
|
|||||||
this.interpolate = new interpolation();
|
this.interpolate = new interpolation();
|
||||||
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel,this.config.general.name);
|
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel,this.config.general.name);
|
||||||
|
|
||||||
this.measurements = new MeasurementContainer({ autoConvert: true });
|
this.measurements = new MeasurementContainer({
|
||||||
this.measurements.setPreferredUnit('flow', 'm3/s');
|
autoConvert: true,
|
||||||
this.measurements.setPreferredUnit('netFlowRate', 'm3/s');
|
preferredUnits: { flow: 'm3/s', netFlowRate: 'm3/s', level: 'm', volume: 'm3' }
|
||||||
this.measurements.setPreferredUnit('level', 'm');
|
});
|
||||||
this.measurements.setPreferredUnit('volume', 'm3');
|
|
||||||
this.childRegistrationUtils = new childRegistrationUtils(this);
|
this.childRegistrationUtils = new childRegistrationUtils(this);
|
||||||
this.machines = {};
|
this.machines = {};
|
||||||
this.stations = {};
|
this.stations = {};
|
||||||
|
this.machineGroups = {};
|
||||||
|
this.predictedFlowChildren = new Map();
|
||||||
|
|
||||||
//variants in determining what gets priority
|
|
||||||
this.flowVariants = ['measured', 'predicted'];
|
this.flowVariants = ['measured', 'predicted'];
|
||||||
this.levelVariants = ['measured', 'predicted'];
|
this.levelVariants = ['measured', 'predicted'];
|
||||||
|
this.volVariants = ['measured', 'predicted'];
|
||||||
this.flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] };
|
this.flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] };
|
||||||
|
|
||||||
this.basin = {};
|
this.mode = this.config.control.mode;
|
||||||
this.state = {
|
this._levelState = { crossed: new Set(), dwellUntil: null };
|
||||||
direction: 'steady',
|
this.state = { direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null };
|
||||||
netFlow: 0,
|
|
||||||
flowSource: null,
|
|
||||||
seconds: null,
|
|
||||||
remainingSource: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const thresholdFromConfig = Number(this.config.general?.flowThreshold);
|
const thresholdFromConfig = Number(this.config.general?.flowThreshold);
|
||||||
this.flowThreshold = Number.isFinite(thresholdFromConfig) ? thresholdFromConfig : 1e-4;
|
this.flowThreshold = Number.isFinite(thresholdFromConfig) ? thresholdFromConfig : 1e-4;
|
||||||
|
|
||||||
this.initBasinProperties();
|
this.initBasinProperties();
|
||||||
this.logger.debug('PumpingStationV2 initialized');
|
this.logger.debug('PumpingStation initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------- Registration --------------------------- */
|
||||||
|
|
||||||
registerChild(child, softwareType) {
|
registerChild(child, softwareType) {
|
||||||
this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`);
|
this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`);
|
||||||
|
|
||||||
@@ -49,45 +56,28 @@ class PumpingStation {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (softwareType === 'machine' || softwareType === 'pumpingStation') {
|
if (softwareType === 'machine') {
|
||||||
|
this.machines[child.config.general.id] = child;
|
||||||
|
} else if (softwareType === 'pumpingstation') {
|
||||||
|
this.stations[child.config.general.id] = child;
|
||||||
|
} else if (softwareType === 'machinegroup') {
|
||||||
|
this.machineGroups[child.config.general.id] = child;
|
||||||
this._registerPredictedFlowChild(child);
|
this._registerPredictedFlowChild(child);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.warn(`Unsupported child software type: ${softwareType}`);
|
if (softwareType === 'machine' || softwareType === 'pumpingstation' || softwareType === 'machinegroup') {
|
||||||
|
this._registerPredictedFlowChild(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
|
||||||
const snapshot = this._takeMeasurementSnapshot();
|
|
||||||
|
|
||||||
this._updatePredictedVolume(snapshot);
|
|
||||||
|
|
||||||
const netFlow = this._selectBestNetFlow(snapshot);
|
|
||||||
//write netflow in measurment container
|
|
||||||
|
|
||||||
const remaining = this._computeRemainingTime(snapshot, netFlow);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
direction: netFlow.direction,
|
|
||||||
netFlow: netFlow.value,
|
|
||||||
flowSource: netFlow.source,
|
|
||||||
seconds: remaining.seconds,
|
|
||||||
remainingSource: remaining.source
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logger.debug(`netflow = ${JSON.stringify(netFlow)}`);
|
|
||||||
this.logger.debug(`Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m') } m`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_registerMeasurementChild(child) {
|
_registerMeasurementChild(child) {
|
||||||
const position = child.config.functionality.positionVsParent;
|
const position = child.config.functionality.positionVsParent;
|
||||||
const measurementType = child.config.asset.type;
|
const measurementType = child.config.asset.type;
|
||||||
const eventName = `${measurementType}.measured.${position}`;
|
const eventName = `${measurementType}.measured.${position}`;
|
||||||
|
|
||||||
child.measurements.emitter.on(eventName, (eventData) => {
|
child.measurements.emitter.on(eventName, (eventData = {}) => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Measurement update ${eventName} <- ${eventData.childName}: ${eventData.value} ${eventData.unit}`
|
`Measurement update ${eventName} <- ${eventData.childName || child.config.general.name}: ${eventData.value} ${eventData.unit}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.measurements
|
this.measurements
|
||||||
@@ -101,31 +91,218 @@ class PumpingStation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_registerPredictedFlowChild(child) {
|
_registerPredictedFlowChild(child) {
|
||||||
const position = child.config.functionality.positionVsParent;
|
const position = (child.config.functionality.positionVsParent || '').toLowerCase();
|
||||||
const childName = child.config.general.name;
|
const childName = child.config.general.name;
|
||||||
|
const childId = child.config.general.id ?? childName;
|
||||||
|
|
||||||
const listener = (eventName, posKey) => {
|
let posKey;
|
||||||
child.measurements.emitter.on(eventName, (eventData) => {
|
let eventNames;
|
||||||
this.logger.debug(
|
switch (position) {
|
||||||
`Predicted flow update from ${childName} (${position}) -> ${eventData.value} ${eventData.unit}`
|
case 'downstream':
|
||||||
);
|
case 'out':
|
||||||
this.measurements
|
case 'atequipment':
|
||||||
.type('flow')
|
posKey = 'out';
|
||||||
.variant('predicted')
|
eventNames = ['flow.predicted.downstream', 'flow.predicted.atequipment'];
|
||||||
.position(posKey)
|
break;
|
||||||
.value(eventData.value, eventData.timestamp, eventData.unit);
|
case 'upstream':
|
||||||
});
|
case 'in':
|
||||||
|
posKey = 'in';
|
||||||
|
eventNames = ['flow.predicted.upstream', 'flow.predicted.atequipment'];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.predictedFlowChildren.has(childId)) {
|
||||||
|
this.predictedFlowChildren.set(childId, { in: 0, out: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = (eventData = {}) => {
|
||||||
|
const unit = eventData.unit || child.config?.general?.unit;
|
||||||
|
const ts = eventData.timestamp || Date.now();
|
||||||
|
|
||||||
|
this.logger.debug(`Emitting for child ${unit} `);
|
||||||
|
this.measurements
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position(posKey)
|
||||||
|
.child(childId)
|
||||||
|
.value(eventData.value, ts, unit);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (position === 'downstream' || position === 'atequipment' || position === 'out') {
|
eventNames.forEach((ev) => child.measurements.emitter.on(ev, handler));
|
||||||
listener('flow.predicted.downstream', 'out');
|
}
|
||||||
} else if (position === 'upstream' || position === 'in') {
|
|
||||||
listener('flow.predicted.downstream', 'in');
|
/* --------------------------- Calibration --------------------------- */
|
||||||
} else {
|
|
||||||
this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`);
|
calibratePredictedVolume(calibratedVol, timestamp = Date.now()) {
|
||||||
|
const volume = this.measurements.type('volume').variant('predicted').position('atequipment').get();
|
||||||
|
const level = this.measurements.type('level').variant('predicted').position('atequipment').get();
|
||||||
|
|
||||||
|
if (volume) {
|
||||||
|
volume.values = [];
|
||||||
|
volume.timestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level) {
|
||||||
|
level.values = [];
|
||||||
|
level.timestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.measurements.type('volume').variant('predicted').position('atequipment').value(calibratedVol, timestamp, 'm3').unit('m3');
|
||||||
|
this.measurements.type('level').variant('predicted').position('atequipment').value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm');
|
||||||
|
|
||||||
|
this._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: timestamp };
|
||||||
|
}
|
||||||
|
|
||||||
|
calibratePredictedLevel(val, timestamp = Date.now(), unit = 'm') {
|
||||||
|
const volumeChain = this.measurements.type('volume').variant('predicted').position('atequipment');
|
||||||
|
const levelChain = this.measurements.type('level').variant('predicted').position('atequipment');
|
||||||
|
|
||||||
|
const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null;
|
||||||
|
if (volumeMeasurement) {
|
||||||
|
volumeMeasurement.values = [];
|
||||||
|
volumeMeasurement.timestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelMeasurement = levelChain.exists() ? levelChain.get() : null;
|
||||||
|
if (levelMeasurement) {
|
||||||
|
levelMeasurement.values = [];
|
||||||
|
levelMeasurement.timestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
levelChain.value(val, timestamp).unit(unit);
|
||||||
|
volumeChain.value(this._calcVolumeFromLevel(val), timestamp, 'm3');
|
||||||
|
|
||||||
|
this._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: timestamp };
|
||||||
|
}
|
||||||
|
|
||||||
|
setManualInflow(value, timestamp = Date.now(), unit) {
|
||||||
|
const num = Number(value);
|
||||||
|
this.measurements.type('flow').variant('predicted').position('in').child('manual-qin').value(num, timestamp, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------- Tick / Control --------------------------- */
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
this._updatePredictedVolume();
|
||||||
|
|
||||||
|
const netFlow = this._selectBestNetFlow();
|
||||||
|
const remaining = this._computeRemainingTime(netFlow);
|
||||||
|
|
||||||
|
this._safetyController(remaining.seconds, netFlow.direction);
|
||||||
|
if (this.safetyControllerActive) return;
|
||||||
|
|
||||||
|
this._controlLogic(netFlow.direction);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
direction: netFlow.direction,
|
||||||
|
netFlow: netFlow.value,
|
||||||
|
flowSource: netFlow.source,
|
||||||
|
seconds: remaining.seconds,
|
||||||
|
remainingSource: remaining.source
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.debug(`netflow = ${JSON.stringify(netFlow)}`);
|
||||||
|
this.logger.debug(
|
||||||
|
`Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m')} m`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMode(newMode){
|
||||||
|
if ( this.config.control.allowedModes.has(newMode) ){
|
||||||
|
const currentMode = this.mode;
|
||||||
|
this.logger.info(`Control mode changing from ${currentMode} to ${newMode}`);
|
||||||
|
this.mode = newMode;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_controlLogic(direction) {
|
||||||
|
switch (this.mode) {
|
||||||
|
case 'levelbased':
|
||||||
|
this._controlLevelBased(direction);
|
||||||
|
break;
|
||||||
|
case 'flowbased':
|
||||||
|
this._controlFlowBased?.();
|
||||||
|
break;
|
||||||
|
case 'manual':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.logger.warn(`Unsupported control mode: ${this.mode}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _controlLevelBased(direction) {
|
||||||
|
const { startLevel, stopLevel } = this.config.control.levelbased;
|
||||||
|
const flowUnit = this.measurements.getUnit('flow');
|
||||||
|
const levelUnit = this.measurements.getUnit('level');
|
||||||
|
|
||||||
|
const level = this._pickVariant('level', this.levelVariants, 'atequipment', levelUnit);
|
||||||
|
if (level == null) {
|
||||||
|
this.logger.warn('No valid level found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level > startLevel && direction === 'filling') {
|
||||||
|
const percControl = this._scaleLevelToFlowPercent(level);
|
||||||
|
this.logger.debug(`Controllevel based => Level ${level} control applying to pump : ${percControl}`);
|
||||||
|
await this._applyMachineLevelControl(percControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level < stopLevel && direction === 'draining') {
|
||||||
|
Object.values(this.machines).forEach((machine) => {
|
||||||
|
const pos = machine?.config?.functionality?.positionVsParent;
|
||||||
|
if ((pos === 'downstream' || pos === 'atequipment') && machine._isOperationalState()) {
|
||||||
|
machine.handleInput('parent', 'execSequence', 'shutdown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown'));
|
||||||
|
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_controlFlowBased() {
|
||||||
|
// placeholder for flow-based logic
|
||||||
|
}
|
||||||
|
|
||||||
|
async _applyMachineGroupLevelControl(percentControl) {
|
||||||
|
if (!this.machineGroups || Object.keys(this.machineGroups).length === 0) return;
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(this.machineGroups).map((group) =>
|
||||||
|
group.handleInput('parent', percentControl).catch((err) => {
|
||||||
|
this.logger.error(`Failed to send level control to group "${group.config.general.name}": ${err.message}`);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _applyMachineLevelControl(percentControl) {
|
||||||
|
const machines = Object.values(this.machines).filter((machine) => {
|
||||||
|
const pos = machine?.config?.functionality?.positionVsParent;
|
||||||
|
return (pos === 'downstream' || pos === 'atequipment');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!machines.length) return;
|
||||||
|
|
||||||
|
const perMachine = percentControl / machines.length;
|
||||||
|
for (const machine of machines) {
|
||||||
|
try {
|
||||||
|
await machine.handleInput('parent', 'execSequence', 'startup');
|
||||||
|
await machine.handleInput('parent', 'execMovement', perMachine);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Failed to start machine "${machine.config.general.name}": ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------- Measurements --------------------------- */
|
||||||
|
|
||||||
_handleMeasurement(measurementType, value, position, context) {
|
_handleMeasurement(measurementType, value, position, context) {
|
||||||
switch (measurementType) {
|
switch (measurementType) {
|
||||||
case 'level':
|
case 'level':
|
||||||
@@ -134,16 +311,13 @@ class PumpingStation {
|
|||||||
case 'pressure':
|
case 'pressure':
|
||||||
this._onPressureMeasurement(position, value, context);
|
this._onPressureMeasurement(position, value, context);
|
||||||
break;
|
break;
|
||||||
case 'flow':
|
|
||||||
// Additional flow-specific logic could go here if needed
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
this.logger.debug(`Unhandled measurement type "${measurementType}", storing only.`);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLevelMeasurement(position, value, context = {}) {
|
_onLevelMeasurement(position, value, context = {}) {
|
||||||
|
this.measurements.type('level').variant('measured').position(position).value(value).unit(context.unit);
|
||||||
const levelSeries = this.measurements.type('level').variant('measured').position(position);
|
const levelSeries = this.measurements.type('level').variant('measured').position(position);
|
||||||
const levelMeters = levelSeries.getCurrentValue('m');
|
const levelMeters = levelSeries.getCurrentValue('m');
|
||||||
if (levelMeters == null) return;
|
if (levelMeters == null) return;
|
||||||
@@ -157,12 +331,7 @@ class PumpingStation {
|
|||||||
100
|
100
|
||||||
);
|
);
|
||||||
|
|
||||||
this.measurements
|
this.measurements.type('volume').variant('measured').position('atequipment').value(volume, context.timestamp, 'm3');
|
||||||
.type('volume')
|
|
||||||
.variant('measured')
|
|
||||||
.position('atequipment')
|
|
||||||
.value(volume, context.timestamp, 'm3');
|
|
||||||
|
|
||||||
this.measurements
|
this.measurements
|
||||||
.type('volumePercent')
|
.type('volumePercent')
|
||||||
.variant('measured')
|
.variant('measured')
|
||||||
@@ -172,281 +341,150 @@ class PumpingStation {
|
|||||||
|
|
||||||
_onPressureMeasurement(position, value, context = {}) {
|
_onPressureMeasurement(position, value, context = {}) {
|
||||||
let kelvinTemp =
|
let kelvinTemp =
|
||||||
this.measurements
|
this.measurements.type('temperature').variant('measured').position('atequipment').getCurrentValue('K') ?? null;
|
||||||
.type('temperature')
|
|
||||||
.variant('measured')
|
|
||||||
.position('atequipment')
|
|
||||||
.getCurrentValue('K') ?? null;
|
|
||||||
|
|
||||||
if (kelvinTemp === null) {
|
if (kelvinTemp === null) {
|
||||||
this.logger.warn('No temperature measurement; assuming 15C for pressure to level conversion.');
|
this.logger.warn('No temperature measurement; assuming 15C for pressure to level conversion.');
|
||||||
this.measurements
|
this.measurements.type('temperature').variant('assumed').position('atequipment').value(15, Date.now(), 'C');
|
||||||
.type('temperature')
|
kelvinTemp = this.measurements.type('temperature').variant('assumed').position('atequipment').getCurrentValue('K');
|
||||||
.variant('assumed')
|
|
||||||
.position('atequipment')
|
|
||||||
.value(15, Date.now(), 'C');
|
|
||||||
kelvinTemp = this.measurements
|
|
||||||
.type('temperature')
|
|
||||||
.variant('assumed')
|
|
||||||
.position('atequipment')
|
|
||||||
.getCurrentValue('K');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kelvinTemp == null) return;
|
if (kelvinTemp == null) return;
|
||||||
|
|
||||||
const density = coolprop.PropsSI('D', 'T', kelvinTemp, 'P', 101325, 'Water');
|
const density = coolprop.PropsSI('D', 'T', kelvinTemp, 'P', 101325, 'Water');
|
||||||
const pressurePa = this.measurements
|
const pressurePa = this.measurements.type('pressure').variant('measured').position(position).getCurrentValue('Pa');
|
||||||
.type('pressure')
|
|
||||||
.variant('measured')
|
|
||||||
.position(position)
|
|
||||||
.getCurrentValue('Pa');
|
|
||||||
|
|
||||||
if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return;
|
if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return;
|
||||||
|
|
||||||
const g = 9.80665;
|
const g = 9.80665;
|
||||||
const level = pressurePa / (density * g);
|
const level = pressurePa / (density * g);
|
||||||
|
|
||||||
this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm');
|
this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm');
|
||||||
}
|
}
|
||||||
|
|
||||||
_takeMeasurementSnapshot() {
|
/* --------------------------- Core Calculations --------------------------- */
|
||||||
const snapshot = {
|
|
||||||
flows: {},
|
|
||||||
levels: {},
|
|
||||||
levelRates: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const variant of this.flowVariants) {
|
_pickVariant(type, variants, position, unit) {
|
||||||
snapshot.flows[variant] = this._snapshotFlowsForVariant(variant);
|
for (const variant of variants) {
|
||||||
}
|
const val = this.measurements.type(type).variant(variant).position(position).getCurrentValue(unit);
|
||||||
|
if (!Number.isFinite(val)) continue;
|
||||||
for (const variant of this.levelVariants) {
|
return val;
|
||||||
snapshot.levels[variant] = this._snapshotLevelForVariant(variant);
|
|
||||||
snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
_snapshotFlowsForVariant(variant) {
|
|
||||||
const inflowSeries = this._locateSeries('flow', variant, this.flowPositions.inflow);
|
|
||||||
const outflowSeries = this._locateSeries('flow', variant, this.flowPositions.outflow);
|
|
||||||
|
|
||||||
return {
|
|
||||||
variant,
|
|
||||||
inflow: this._seriesSamples(inflowSeries),
|
|
||||||
outflow: this._seriesSamples(outflowSeries)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_snapshotLevelForVariant(variant) {
|
|
||||||
const levelSeries = this._locateSeries('level', variant, ['atequipment']);
|
|
||||||
return {
|
|
||||||
variant,
|
|
||||||
samples: this._seriesSamples(levelSeries)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_seriesSamples(seriesInfo) {
|
|
||||||
if (!seriesInfo) {
|
|
||||||
return { exists: false, measurement: null, current: null, previous: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const current = seriesInfo.measurement.getLaggedSample(0); // newest
|
|
||||||
const previous = seriesInfo.measurement.getLaggedSample(1); // previous
|
|
||||||
return {
|
|
||||||
exists: Boolean(current),
|
|
||||||
measurement: seriesInfo.measurement,
|
|
||||||
current,
|
|
||||||
previous
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.debug(
|
|
||||||
`Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}`
|
|
||||||
);
|
|
||||||
return { exists: false, measurement: seriesInfo.measurement, current: null, previous: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_locateSeries(type, variant, positions) {
|
|
||||||
for (const position of positions) {
|
|
||||||
try {
|
|
||||||
const chain = this.measurements.type(type).variant(variant).position(position);
|
|
||||||
if (!chain.exists({ requireValues: true })) continue;
|
|
||||||
|
|
||||||
const measurement = chain.get();
|
|
||||||
if (!measurement) continue;
|
|
||||||
|
|
||||||
return { type, variant, position, measurement };
|
|
||||||
} catch (err) {
|
|
||||||
// ignore missing combinations
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_estimateLevelRate(levelSnapshot) {
|
//scaled for robin min 2039 - 2960 max 53.04
|
||||||
if (!levelSnapshot.samples.exists){ return null};
|
_scaleLevelToFlowPercent(level) {
|
||||||
const { current, previous } = levelSnapshot.samples;
|
const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased;
|
||||||
if (!current || !previous || previous.timestamp == null){return null};
|
this.logger.debug(`Scaling minflow level : ${minFlowLevel} and maxflowLevel : ${maxFlowLevel}`);
|
||||||
|
return this.interpolate.interpolate_lin_single_point(level, minFlowLevel, maxFlowLevel, 0, 100);
|
||||||
const deltaT = (current.timestamp - previous.timestamp) / 1000;
|
|
||||||
if (!Number.isFinite(deltaT) || deltaT <= 0){ return null};
|
|
||||||
|
|
||||||
const deltaLevel = current.value - previous.value;
|
|
||||||
return deltaLevel / deltaT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectBestNetFlow(snapshot) {
|
_levelRate(variant) {
|
||||||
for (const variant of this.flowVariants) {
|
const chain = this.measurements.type('level').variant(variant).position('atequipment');
|
||||||
const flow = snapshot.flows[variant];
|
if (!chain.exists({ requireValues: true })) return null;
|
||||||
|
const m = chain.get();
|
||||||
if (!flow.inflow.exists && !flow.outflow.exists) continue;
|
const current = m?.getLaggedSample?.(0);
|
||||||
|
const previous = m?.getLaggedSample?.(1);
|
||||||
const inflow = flow.inflow.current?.value ?? 0;
|
if (!current || !previous || previous.timestamp == null) return null;
|
||||||
const outflow = flow.outflow.current?.value ?? 0;
|
const dt = (current.timestamp - previous.timestamp) / 1000;
|
||||||
const net = inflow - outflow; // positive => filling
|
if (!Number.isFinite(dt) || dt <= 0) return null;
|
||||||
|
return (current.value - previous.value) / dt;
|
||||||
this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net).unit('m3/s');
|
|
||||||
this.logger.debug(`inflow : ${inflow} - outflow : ${outflow}`);
|
|
||||||
|
|
||||||
return { value: net,source: variant,direction: this._deriveDirection(net) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback using level trend
|
|
||||||
for (const variant of this.levelVariants) {
|
|
||||||
const levelRate = snapshot.levelRates[variant];
|
|
||||||
if (!Number.isFinite(levelRate)) continue;
|
|
||||||
|
|
||||||
const netFlow = levelRate * this.basin.surfaceArea;
|
|
||||||
return {
|
|
||||||
value: netFlow,
|
|
||||||
source: `level:${variant}`,
|
|
||||||
direction: this._deriveDirection(netFlow)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn('No usable measurements to compute net flow; assuming steady.');
|
|
||||||
return { value: 0, source: null, direction: 'steady' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeRemainingTime(snapshot, netFlow) {
|
_updatePredictedVolume() {
|
||||||
if (!netFlow || Math.abs(netFlow.value) < this.flowThreshold) {
|
const flowUnit = 'm3/s'; // this has to be in m3/s for the actions below
|
||||||
return { seconds: null, source: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
|
||||||
if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) {
|
|
||||||
this.logger.warn('Invalid basin surface area.');
|
|
||||||
return { seconds: null, source: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const variant of this.levelVariants) {
|
|
||||||
const levelSnap = snapshot.levels[variant];
|
|
||||||
const current = levelSnap.samples.current?.value ?? null;
|
|
||||||
if (!Number.isFinite(current)) continue;
|
|
||||||
|
|
||||||
const remainingHeight =
|
|
||||||
netFlow.value > 0
|
|
||||||
? Math.max(heightOverflow - current, 0)
|
|
||||||
: Math.max(current - heightOutlet, 0);
|
|
||||||
|
|
||||||
const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value);
|
|
||||||
if (!Number.isFinite(seconds)) continue;
|
|
||||||
|
|
||||||
return { seconds, source: `${netFlow.source}/${variant}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn('No level data available to compute remaining time.');
|
|
||||||
return { seconds: null, source: netFlow.source };
|
|
||||||
}
|
|
||||||
|
|
||||||
_updatePredictedVolume(snapshot) {
|
|
||||||
const predicted = snapshot.flows.predicted;
|
|
||||||
if (!predicted) return;
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const inflowSample = predicted.inflow.current ?? predicted.inflow.previous ?? null;
|
|
||||||
const outflowSample = predicted.outflow.current ?? predicted.outflow.previous ?? null;
|
const inflow = this.measurements.sum('flow', 'predicted', this.flowPositions.inflow, flowUnit) || 0;
|
||||||
|
const outflow = this.measurements.sum('flow', 'predicted', this.flowPositions.outflow, flowUnit) || 0;
|
||||||
|
|
||||||
if (!this._predictedFlowState) {
|
if (!this._predictedFlowState) {
|
||||||
this._predictedFlowState = {
|
this._predictedFlowState = { inflow, outflow, lastTimestamp: now };
|
||||||
inflow: inflowSample?.value ?? 0,
|
|
||||||
outflow: outflowSample?.value ?? 0,
|
|
||||||
lastTimestamp: inflowSample?.timestamp ?? outflowSample?.timestamp ?? now
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inflowSample) this._predictedFlowState.inflow = inflowSample.value;
|
const timestampPrev = this._predictedFlowState.lastTimestamp ?? now;
|
||||||
if (outflowSample) this._predictedFlowState.outflow = outflowSample.value;
|
const deltaSeconds = Math.max((now - timestampPrev) / 1000, 0);
|
||||||
|
const netVolumeChange = deltaSeconds > 0 ? (inflow - outflow) * deltaSeconds : 0;
|
||||||
const latestObservedTimestamp =
|
|
||||||
inflowSample?.timestamp ?? outflowSample?.timestamp ?? this._predictedFlowState.lastTimestamp;
|
|
||||||
|
|
||||||
const timestampPrev = this._predictedFlowState.lastTimestamp ?? latestObservedTimestamp;
|
|
||||||
|
|
||||||
let timestampNow = latestObservedTimestamp;
|
|
||||||
if (!Number.isFinite(timestampNow) || timestampNow <= timestampPrev) {
|
|
||||||
timestampNow = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deltaSeconds = (timestampNow - timestampPrev) / 1000;
|
|
||||||
if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) {
|
|
||||||
deltaSeconds = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let netVolumeChange = 0;
|
|
||||||
if (deltaSeconds > 0) {
|
|
||||||
const avgInflow = inflowSample ? inflowSample.value : this._predictedFlowState.inflow;
|
|
||||||
const avgOutflow = outflowSample ? outflowSample.value : this._predictedFlowState.outflow;
|
|
||||||
netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeTimestamp = timestampPrev + Math.max(deltaSeconds, 0) * 1000;
|
|
||||||
|
|
||||||
const volumeSeries = this.measurements
|
|
||||||
.type('volume')
|
|
||||||
.variant('predicted')
|
|
||||||
.position('atEquipment');
|
|
||||||
|
|
||||||
const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol;
|
|
||||||
|
|
||||||
|
const volumeSeries = this.measurements.type('volume').variant('predicted').position('atequipment');
|
||||||
|
const currentVolume = volumeSeries.getCurrentValue('m3');
|
||||||
|
|
||||||
const nextVolume = currentVolume + netVolumeChange;
|
const nextVolume = currentVolume + netVolumeChange;
|
||||||
|
const writeTimestamp = timestampPrev + deltaSeconds * 1000;
|
||||||
|
|
||||||
volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3');
|
volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3'); //olifant
|
||||||
|
|
||||||
const nextLevel = this._calcLevelFromVolume(nextVolume);
|
const nextLevel = this._calcLevelFromVolume(nextVolume);
|
||||||
this.measurements
|
this.measurements
|
||||||
.type('level')
|
.type('level')
|
||||||
.variant('predicted')
|
.variant('predicted')
|
||||||
.position('atEquipment')
|
.position('atequipment')
|
||||||
.value(nextLevel, writeTimestamp, 'm')
|
.value(nextLevel, writeTimestamp, 'm')
|
||||||
.unit('m');
|
.unit('m');
|
||||||
|
|
||||||
//calc how full this is in procen using minVol vs maxVolOverflow
|
|
||||||
const percent = this.interpolate.interpolate_lin_single_point(
|
const percent = this.interpolate.interpolate_lin_single_point(
|
||||||
currentVolume,
|
nextVolume,
|
||||||
this.basin.minVol,
|
this.basin.minVol,
|
||||||
this.basin.maxVolOverflow,
|
this.basin.maxVolOverflow,
|
||||||
0,
|
0,
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
|
|
||||||
//store this percent value
|
|
||||||
this.measurements
|
this.measurements
|
||||||
.type('volumePercent')
|
.type('volumePercent')
|
||||||
.variant('predicted')
|
.variant('predicted')
|
||||||
.position('atequipment')
|
.position('atequipment')
|
||||||
.value(percent);
|
.value(percent, writeTimestamp, '%');
|
||||||
|
|
||||||
this._predictedFlowState.lastTimestamp = writeTimestamp;
|
this._predictedFlowState = { inflow, outflow, lastTimestamp: writeTimestamp };
|
||||||
}
|
}
|
||||||
|
|
||||||
_averageSampleValues(sampleA, sampleB) {
|
_selectBestNetFlow() {
|
||||||
const values = [sampleA?.value, sampleB?.value].filter((v) => Number.isFinite(v));
|
const type = 'flow';
|
||||||
if (!values.length) return 0;
|
const unit = this.measurements.getUnit(type) || 'm3/s';
|
||||||
return values.reduce((acc, val) => acc + val, 0) / values.length;
|
|
||||||
|
for (const variant of this.flowVariants) {
|
||||||
|
const bucket = this.measurements.measurements?.[type]?.[variant];
|
||||||
|
if (!bucket || Object.keys(bucket).length === 0) continue;
|
||||||
|
|
||||||
|
const inflow = this.measurements.sum(type, variant, this.flowPositions.inflow, unit) || 0;
|
||||||
|
const outflow = this.measurements.sum(type, variant, this.flowPositions.outflow, unit) || 0;
|
||||||
|
if (Math.abs(inflow) < this.flowThreshold && Math.abs(outflow) < this.flowThreshold) continue;
|
||||||
|
|
||||||
|
const net = inflow - outflow;
|
||||||
|
this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net, Date.now(), unit);
|
||||||
|
return { value: net, source: variant, direction: this._deriveDirection(net) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: level trend
|
||||||
|
for (const variant of this.levelVariants) {
|
||||||
|
const rate = this._levelRate(variant);
|
||||||
|
if (!Number.isFinite(rate)) continue;
|
||||||
|
const netFlow = rate * this.basin.surfaceArea;
|
||||||
|
return { value: netFlow, source: `level:${variant}`, direction: this._deriveDirection(netFlow) };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn('No usable measurements to compute net flow; assuming steady.');
|
||||||
|
return { value: 0, source: null, direction: 'steady' };
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeRemainingTime(netFlow) {
|
||||||
|
if (!netFlow || Math.abs(netFlow.value) < this.flowThreshold) return { seconds: null, source: null };
|
||||||
|
|
||||||
|
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
||||||
|
if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) return { seconds: null, source: null };
|
||||||
|
|
||||||
|
for (const variant of this.levelVariants) {
|
||||||
|
const lvl = this.measurements.type('level').variant(variant).position('atequipment').getCurrentValue('m');
|
||||||
|
if (!Number.isFinite(lvl)) continue;
|
||||||
|
|
||||||
|
const remainingHeight = netFlow.value > 0 ? Math.max(heightOverflow - lvl, 0) : Math.max(lvl - heightOutlet, 0);
|
||||||
|
const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value);
|
||||||
|
if (!Number.isFinite(seconds)) continue;
|
||||||
|
|
||||||
|
return { seconds, source: `${netFlow.source}/${variant}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { seconds: null, source: netFlow.source };
|
||||||
}
|
}
|
||||||
|
|
||||||
_deriveDirection(netFlow) {
|
_deriveDirection(netFlow) {
|
||||||
@@ -455,11 +493,77 @@ class PumpingStation {
|
|||||||
return 'steady';
|
return 'steady';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* --------------------------- Safety --------------------------- */
|
||||||
/* Basin Calculations */
|
|
||||||
/* ------------------------------------------------------------------ */
|
_safetyController(remainingTime, direction) {
|
||||||
|
this.safetyControllerActive = false;
|
||||||
|
|
||||||
|
const volUnit = this.measurements.getUnit('volume');
|
||||||
|
const vol = this._pickVariant('volume', this.volVariants, 'atequipment', volUnit);
|
||||||
|
if (vol == null) {
|
||||||
|
Object.values(this.machines).forEach((machine) => machine.handleInput('parent', 'execSequence', 'shutdown'));
|
||||||
|
this.logger.warn('No volume data available to safe guard system; shutting down all machines.');
|
||||||
|
this.safetyControllerActive = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
enableDryRunProtection,
|
||||||
|
dryRunThresholdPercent,
|
||||||
|
enableOverfillProtection,
|
||||||
|
overfillThresholdPercent,
|
||||||
|
timeleftToFullOrEmptyThresholdSeconds
|
||||||
|
} = this.config.safety || {};
|
||||||
|
|
||||||
|
const dryRunEnabled = Boolean(enableDryRunProtection);
|
||||||
|
const overfillEnabled = Boolean(enableOverfillProtection);
|
||||||
|
const timeProtectionEnabled = timeleftToFullOrEmptyThresholdSeconds > 0;
|
||||||
|
const triggerHighVol = this.basin.maxVolOverflow * ((Number(overfillThresholdPercent) || 0) / 100);
|
||||||
|
const triggerLowVol = this.basin.minVol * (1 + ((Number(dryRunThresholdPercent) || 0) / 100));
|
||||||
|
|
||||||
|
if (direction === 'draining') {
|
||||||
|
const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds;
|
||||||
|
const dryRunTriggered = dryRunEnabled && vol < triggerLowVol;
|
||||||
|
if (timeTriggered || dryRunTriggered) {
|
||||||
|
Object.values(this.machines).forEach((machine) => {
|
||||||
|
const pos = machine?.config?.functionality?.positionVsParent;
|
||||||
|
if ((pos === 'downstream' || pos === 'atequipment') && machine._isOperationalState()) {
|
||||||
|
machine.handleInput('parent', 'execSequence', 'shutdown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown'));
|
||||||
|
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||||
|
this.logger.warn(
|
||||||
|
`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down downstream equipment`
|
||||||
|
);
|
||||||
|
this.safetyControllerActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === 'filling') {
|
||||||
|
const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds;
|
||||||
|
const overfillTriggered = overfillEnabled && vol > triggerHighVol;
|
||||||
|
if (timeTriggered || overfillTriggered) {
|
||||||
|
Object.values(this.machines).forEach((machine) => {
|
||||||
|
const pos = machine?.config?.functionality?.positionVsParent;
|
||||||
|
if (pos === 'upstream' && machine._isOperationalState()) {
|
||||||
|
machine.handleInput('parent', 'execSequence', 'shutdown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines());
|
||||||
|
Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown'));
|
||||||
|
this.logger.warn(
|
||||||
|
`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down upstream equipment`
|
||||||
|
);
|
||||||
|
this.safetyControllerActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------- Basin --------------------------- */
|
||||||
|
|
||||||
initBasinProperties() {
|
initBasinProperties() {
|
||||||
|
const minHeightBasedOn = this.config.hydraulics.minHeightBasedOn;
|
||||||
const volEmptyBasin = this.config.basin.volume;
|
const volEmptyBasin = this.config.basin.volume;
|
||||||
const heightBasin = this.config.basin.height;
|
const heightBasin = this.config.basin.height;
|
||||||
const heightInlet = this.config.basin.heightInlet;
|
const heightInlet = this.config.basin.heightInlet;
|
||||||
@@ -469,8 +573,9 @@ class PumpingStation {
|
|||||||
const surfaceArea = volEmptyBasin / heightBasin;
|
const surfaceArea = volEmptyBasin / heightBasin;
|
||||||
const maxVol = heightBasin * surfaceArea;
|
const maxVol = heightBasin * surfaceArea;
|
||||||
const maxVolOverflow = heightOverflow * surfaceArea;
|
const maxVolOverflow = heightOverflow * surfaceArea;
|
||||||
const minVol = heightOutlet * surfaceArea;
|
const minVolOut = heightOutlet * surfaceArea;
|
||||||
const minVolOut = heightInlet * surfaceArea;
|
const minVolIn = heightInlet * surfaceArea;
|
||||||
|
const minVol = minHeightBasedOn === 'inlet' ? minVolIn : minVolOut;
|
||||||
|
|
||||||
this.basin = {
|
this.basin = {
|
||||||
volEmptyBasin,
|
volEmptyBasin,
|
||||||
@@ -481,20 +586,13 @@ class PumpingStation {
|
|||||||
surfaceArea,
|
surfaceArea,
|
||||||
maxVol,
|
maxVol,
|
||||||
maxVolOverflow,
|
maxVolOverflow,
|
||||||
|
minVolIn,
|
||||||
|
minVolOut,
|
||||||
minVol,
|
minVol,
|
||||||
minVolOut
|
minHeightBasedOn
|
||||||
};
|
};
|
||||||
|
|
||||||
this.measurements
|
this.measurements.type('volume').variant('predicted').position('atequipment').value(minVol).unit('m3');
|
||||||
.type('volume')
|
|
||||||
.variant('predicted')
|
|
||||||
.position('atEquipment')
|
|
||||||
.value(maxVolOverflow)
|
|
||||||
.unit('m3');
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_calcVolumeFromLevel(level) {
|
_calcVolumeFromLevel(level) {
|
||||||
@@ -505,20 +603,10 @@ class PumpingStation {
|
|||||||
return Math.max(volume, 0) / this.basin.surfaceArea;
|
return Math.max(volume, 0) / this.basin.surfaceArea;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* --------------------------- Output --------------------------- */
|
||||||
/* Output */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
getOutput() {
|
getOutput() {
|
||||||
const output = {};
|
const output = this.measurements.getFlattenedOutput();
|
||||||
Object.entries(this.measurements.measurements).forEach(([type, variants]) => {
|
|
||||||
Object.entries(variants).forEach(([variant, positions]) => {
|
|
||||||
Object.entries(positions).forEach(([position, measurement]) => {
|
|
||||||
output[`${type}.${variant}.${position}`] = measurement.getCurrentValue();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
output.direction = this.state.direction;
|
output.direction = this.state.direction;
|
||||||
output.flowSource = this.state.flowSource;
|
output.flowSource = this.state.flowSource;
|
||||||
output.timeleft = this.state.seconds;
|
output.timeleft = this.state.seconds;
|
||||||
@@ -529,16 +617,17 @@ class PumpingStation {
|
|||||||
output.minVol = this.basin.minVol;
|
output.minVol = this.basin.minVol;
|
||||||
output.maxVolOverflow = this.basin.maxVolOverflow;
|
output.maxVolOverflow = this.basin.maxVolOverflow;
|
||||||
output.minVolOut = this.basin.minVolOut;
|
output.minVolOut = this.basin.minVolOut;
|
||||||
|
output.minVolIn = this.basin.minVolIn;
|
||||||
|
output.minHeightBasedOn = this.basin.minHeightBasedOn;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PumpingStation;
|
module.exports = PumpingStation;
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
/* Example usage */
|
/* Example usage */
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
/*
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const Measurement = require('../../measurement/src/specificClass');
|
const Measurement = require('../../measurement/src/specificClass');
|
||||||
const RotatingMachine = require('../../rotatingMachine/src/specificClass');
|
const RotatingMachine = require('../../rotatingMachine/src/specificClass');
|
||||||
@@ -565,6 +654,10 @@ if (require.main === module) {
|
|||||||
hydraulics: {
|
hydraulics: {
|
||||||
refHeight: 'NAP',
|
refHeight: 'NAP',
|
||||||
basinBottomRef: 0
|
basinBottomRef: 0
|
||||||
|
},
|
||||||
|
safety: {
|
||||||
|
enableDryRunProtection:false,
|
||||||
|
enableOverfillProtection:false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -664,18 +757,18 @@ if (require.main === module) {
|
|||||||
(async function demo() {
|
(async function demo() {
|
||||||
const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo'));
|
const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo'));
|
||||||
const pump1 = new RotatingMachine(createMachineConfig('Pump1','downstream'), createMachineStateConfig());
|
const pump1 = new RotatingMachine(createMachineConfig('Pump1','downstream'), createMachineStateConfig());
|
||||||
const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), createMachineStateConfig());
|
//const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), createMachineStateConfig());
|
||||||
|
|
||||||
const levelSensor = new Measurement(createLevelMeasurementConfig('WetWellLevel'));
|
//const levelSensor = new Measurement(createLevelMeasurementConfig('WetWellLevel'));
|
||||||
const inflowSensor = new Measurement(createFlowMeasurementConfig('InfluentFlow', 'in'));
|
//const inflowSensor = new Measurement(createFlowMeasurementConfig('InfluentFlow', 'in'));
|
||||||
const outflowSensor = new Measurement(createFlowMeasurementConfig('PumpDischargeFlow', 'out'));
|
//const outflowSensor = new Measurement(createFlowMeasurementConfig('PumpDischargeFlow', 'out'));
|
||||||
|
|
||||||
//station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
//station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
||||||
//station.childRegistrationUtils.registerChild(inflowSensor, inflowSensor.config.functionality.softwareType);
|
//station.childRegistrationUtils.registerChild(inflowSensor, inflowSensor.config.functionality.softwareType);
|
||||||
//station.childRegistrationUtils.registerChild(outflowSensor, outflowSensor.config.functionality.softwareType);
|
//station.childRegistrationUtils.registerChild(outflowSensor, outflowSensor.config.functionality.softwareType);
|
||||||
|
|
||||||
station.childRegistrationUtils.registerChild(pump1, 'machine');
|
station.childRegistrationUtils.registerChild(pump1, 'machine');
|
||||||
station.childRegistrationUtils.registerChild(pump2, 'machine');
|
//station.childRegistrationUtils.registerChild(pump2, 'machine');
|
||||||
|
|
||||||
// Seed initial measurements
|
// Seed initial measurements
|
||||||
|
|
||||||
@@ -683,24 +776,26 @@ if (require.main === module) {
|
|||||||
//seedSample(inflowSensor, 'flow', 0.35, 'm3/s');
|
//seedSample(inflowSensor, 'flow', 0.35, 'm3/s');
|
||||||
//seedSample(outflowSensor, 'flow', 0.20, 'm3/s');
|
//seedSample(outflowSensor, 'flow', 0.20, 'm3/s');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setInterval(
|
setInterval(
|
||||||
() => station.tick(), 1000);
|
() => station.tick(), 1000);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
console.log('Initial state:', station.state);
|
console.log('Initial state:', station.state);
|
||||||
|
station.setManualInflow(300,Date.now(),'l/s');
|
||||||
|
station.calibratePredictedVolume(3.4);
|
||||||
|
//await pump1.handleInput('parent', 'execSequence', 'startup');
|
||||||
|
//await pump1.handleInput('parent', 'execMovement', 10);
|
||||||
|
//
|
||||||
|
//await pump2.handleInput('parent', 'execSequence', 'startup');
|
||||||
|
//await pump2.handleInput('parent', 'execMovement', 10);
|
||||||
|
|
||||||
await pump1.handleInput('parent', 'execSequence', 'startup');
|
console.log('Station state:', station.state);
|
||||||
await pump1.handleInput('parent', 'execMovement', 10);
|
console.log('Station output:', station.getOutput());
|
||||||
|
})().catch((err) => {
|
||||||
await pump2.handleInput('parent', 'execSequence', 'startup');
|
console.error('Demo failed:', err);
|
||||||
await pump2.handleInput('parent', 'execMovement', 10);
|
});
|
||||||
|
|
||||||
|
|
||||||
console.log('Station state:', station.state);
|
|
||||||
console.log('Station output:', station.getOutput());
|
|
||||||
})().catch((err) => {
|
|
||||||
console.error('Demo failed:', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
//*/
|
//*/
|
||||||
Reference in New Issue
Block a user