Compare commits
2 Commits
d91609b3a4
...
321ea33bf7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
321ea33bf7 | ||
|
|
288bd244dd |
@@ -53,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 }
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -129,6 +138,32 @@
|
|||||||
: 0;
|
: 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 ------------------- //
|
||||||
},
|
},
|
||||||
oneditsave: function () {
|
oneditsave: function () {
|
||||||
@@ -151,6 +186,18 @@
|
|||||||
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.enableDryRunProtection = document.getElementById("node-input-enableDryRunProtection").checked;
|
||||||
node.enableOverfillProtection = document.getElementById("node-input-enableOverfillProtection").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');
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -160,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;" />
|
||||||
@@ -169,7 +216,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -195,6 +242,50 @@
|
|||||||
|
|
||||||
<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">
|
<div class="form-row">
|
||||||
<label for="node-input-minHeightBasedOn"><i class="fa fa-arrows-v"></i> Minimum Height Based On</label>
|
<label for="node-input-minHeightBasedOn"><i class="fa fa-arrows-v"></i> Minimum Height Based On</label>
|
||||||
@@ -217,6 +308,8 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<h4>Safety</h4>
|
||||||
|
|
||||||
<!-- Safety settings -->
|
<!-- Safety settings -->
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-timeleftToFullOrEmptyThresholdSeconds"><i class="fa fa-clock-o"></i> Time To Empty/Full (s)</label>
|
<label for="node-input-timeleftToFullOrEmptyThresholdSeconds"><i class="fa fa-clock-o"></i> Time To Empty/Full (s)</label>
|
||||||
@@ -246,7 +339,7 @@
|
|||||||
<label for="node-input-overfillThresholdPercent" style="padding-left:20px;">High Volume Threshold (%)</label>
|
<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" />
|
<input type="number" id="node-input-overfillThresholdPercent" min="0" max="100" step="0.1" />
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ class nodeClass {
|
|||||||
minHeightBasedOn: uiConfig.minHeightBasedOn,
|
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:{
|
safety:{
|
||||||
enableDryRunProtection: uiConfig.enableDryRunProtection,
|
enableDryRunProtection: uiConfig.enableDryRunProtection,
|
||||||
dryRunThresholdPercent: uiConfig.dryRunThresholdPercent,
|
dryRunThresholdPercent: uiConfig.dryRunThresholdPercent,
|
||||||
@@ -214,8 +223,18 @@ class nodeClass {
|
|||||||
.variant('measured')
|
.variant('measured')
|
||||||
.position('atequipment')
|
.position('atequipment')
|
||||||
.getCurrentValue('m3');
|
.getCurrentValue('m3');
|
||||||
this.source.calibratePredictedVolume(calibratedVolume);
|
this.source.calibratePredictedVolume(calibratedVolume);
|
||||||
break;
|
break;
|
||||||
|
case 'q_in': {
|
||||||
|
// payload can be number or { value, unit, timestamp }
|
||||||
|
const val = Number(msg.payload);
|
||||||
|
const unit = msg?.unit || 'l/s';
|
||||||
|
const ts = msg?.timestamp || Date.now();
|
||||||
|
this.source.setManualInflow(val, ts, unit);
|
||||||
|
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,convert} = require('generalFunctions');
|
||||||
|
|
||||||
class PumpingStation {
|
class PumpingStation {
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
@@ -11,11 +11,12 @@ 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 = {};
|
||||||
@@ -192,52 +193,64 @@ class PumpingStation {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _controlLevelBased(snapshot, remainingTime) {
|
_scaleLevelToFlowPercent(level,minflow,maxflow) {
|
||||||
|
const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased;
|
||||||
|
const output = this.interpolate.interpolate_lin_single_point(level,minFlowLevel,maxFlowLevel,minflow,maxflow);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// current volume as a percentage of usable capacity
|
async _controlLevelBased(snapshot, direction) {
|
||||||
const vol = this._resolveVolume(snapshot);
|
const { startLevel, stopLevel } = this.config.control.levelbased;
|
||||||
if (vol == null) {
|
const flowUnit = this.measurements.getUnit('flow'); // use container as source of truth
|
||||||
this.logger.warn('No valid volume found for level-based control');
|
|
||||||
|
let percControl = 0;
|
||||||
|
|
||||||
|
const level = (snap) => {
|
||||||
|
for (const variant of this.levelVariants) {
|
||||||
|
const levelSnap = snap.levels?.[variant];
|
||||||
|
if (levelSnap?.samples?.current?.value !== undefined) {
|
||||||
|
return levelSnap.samples.current.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelVal = level(snapshot);
|
||||||
|
if (levelVal == null || !Number.isFinite(levelVal)) {
|
||||||
|
this.logger.warn('No valid level found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { thresholds, timeThresholdSeconds } = this.config.control.levelbased;
|
if (levelVal > startLevel && direction === 'filling') {
|
||||||
const percentFull = (vol / this.basin.maxVolOverflow) * 100;
|
let sumFlow = 0;
|
||||||
|
let minTotalFlow = Infinity;
|
||||||
|
Object.values(this.machines).forEach((m) => {
|
||||||
|
const max = m.measurements.type('flow').variant('predicted').position('max').getCurrentValue(flowUnit);
|
||||||
|
const min = m.measurements.type('flow').variant('predicted').position('min').getCurrentValue(flowUnit);
|
||||||
|
if (Number.isFinite(max)) sumFlow += max;
|
||||||
|
if (Number.isFinite(min) && min < minTotalFlow) minTotalFlow = min;
|
||||||
|
});
|
||||||
|
|
||||||
// pick thresholds that are now crossed but were not crossed before
|
this.logger.debug(`showing level : ${levelVal}, minTotalFlow: ${minTotalFlow}, sumFlow ${sumFlow}`);
|
||||||
const newlyCrossed = thresholds.filter(t => percentFull >= t && !this._levelState.crossed.has(t));
|
percControl = this._scaleLevelToFlowPercent(levelVal, minTotalFlow, sumFlow);
|
||||||
this.logger.debug(`Level-based control: vol=${vol.toFixed(2)} m³ (${percentFull.toFixed(1)}%), newly crossed thresholds: [${newlyCrossed.join(', ')}]`);
|
await this._applyIdleMachineLevelControl(percControl);
|
||||||
if (!newlyCrossed.length) return;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (!this._levelState.dwellUntil) {
|
|
||||||
this._levelState.dwellUntil = now + timeThresholdSeconds * 1000;
|
|
||||||
this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`);
|
if (levelVal < stopLevel && direction === 'draining') {
|
||||||
if (now < this._levelState.dwellUntil) return; // still waiting
|
Object.entries(this.machines).forEach(([machineId, machine]) => {
|
||||||
|
const position = machine?.config?.functionality?.positionVsParent;
|
||||||
this._levelState.dwellUntil = null; // dwell satisfied, let pumps start
|
if ((position === 'downstream' || position === 'atEquipment') && machine._isOperationalState()) {
|
||||||
|
machine.handleInput('parent', 'execSequence', 'shutdown');
|
||||||
this._levelState.dwellUntil = null;
|
}
|
||||||
|
});
|
||||||
newlyCrossed.forEach((threshold) => this._levelState.crossed.add(threshold));
|
Object.entries(this.stations).forEach(([stationId, station]) => {
|
||||||
|
station.handleInput('parent', 'execSequence', 'shutdown');
|
||||||
const percentControl = this._calculateLevelControlPercent(thresholds);
|
});
|
||||||
if (percentControl <= 0) {
|
Object.entries(this.machineGroups).forEach(([groupId, group]) => {
|
||||||
this.logger.debug('Level-based control: percent control resolved to 0%, skipping commands');
|
group.turnOffAllMachines();
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(
|
|
||||||
`level-based control: thresholds [${newlyCrossed.join(', ')}]% reached, requesting ${percentControl.toFixed(1)}% control (vol=${vol.toFixed(2)} m³)`
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._applyMachineGroupLevelControl(percentControl);
|
|
||||||
await this._applyIdleMachineLevelControl(percentControl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _applyMachineGroupLevelControl(percentControl) {
|
async _applyMachineGroupLevelControl(percentControl) {
|
||||||
@@ -275,14 +288,6 @@ class PumpingStation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_calculateLevelControlPercent(thresholds = []) {
|
|
||||||
if (!thresholds.length) return 0;
|
|
||||||
const total = thresholds.length;
|
|
||||||
const crossed = this._levelState.crossed.size;
|
|
||||||
const pct = (crossed / total) * 100;
|
|
||||||
return Math.min(100, Math.max(0, pct));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_resolveVolume(snapshot) {
|
_resolveVolume(snapshot) {
|
||||||
for (const variant of this.volVariants) {
|
for (const variant of this.volVariants) {
|
||||||
@@ -300,13 +305,13 @@ class PumpingStation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//control logic
|
//control logic
|
||||||
_controlLogic(snapshot, remainingTime){
|
_controlLogic(snapshot,direction){
|
||||||
const mode = this.mode;
|
const mode = this.mode;
|
||||||
|
|
||||||
switch(mode){
|
switch(mode){
|
||||||
case "levelbased":
|
case "levelbased":
|
||||||
this.logger.debug(`Executing level-based control logic`);
|
this.logger.debug(`Executing level-based control logic`);
|
||||||
this._controlLevelBased(snapshot, remainingTime);
|
this._controlLevelBased(snapshot,direction);
|
||||||
break;
|
break;
|
||||||
case "flowbased":
|
case "flowbased":
|
||||||
this._controlFlowBased();
|
this._controlFlowBased();
|
||||||
@@ -332,6 +337,11 @@ class PumpingStation {
|
|||||||
.variant('predicted')
|
.variant('predicted')
|
||||||
.position('atequipment');
|
.position('atequipment');
|
||||||
|
|
||||||
|
const levelChain = this.measurements
|
||||||
|
.type('level')
|
||||||
|
.variant('predicted')
|
||||||
|
.position('atequipment');
|
||||||
|
|
||||||
//if we have existing values clear them out
|
//if we have existing values clear them out
|
||||||
const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null;
|
const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null;
|
||||||
if (volumeMeasurement) {
|
if (volumeMeasurement) {
|
||||||
@@ -339,20 +349,51 @@ class PumpingStation {
|
|||||||
volumeMeasurement.timestamps = [];
|
volumeMeasurement.timestamps = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const levelMeasurement = levelChain.exists() ? levelChain.get() : null;
|
||||||
|
if (levelMeasurement) {
|
||||||
|
levelMeasurement.values = [];
|
||||||
|
levelMeasurement.timestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3');
|
volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3');
|
||||||
|
|
||||||
|
levelChain.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
|
const levelChain = this.measurements
|
||||||
.type('level')
|
.type('level')
|
||||||
.variant('predicted')
|
.variant('predicted')
|
||||||
.position('atequipment');
|
.position('atequipment');
|
||||||
|
|
||||||
|
//if we have existing values clear them out
|
||||||
|
const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null;
|
||||||
|
if (volumeMeasurement) {
|
||||||
|
volumeMeasurement.values = [];
|
||||||
|
volumeMeasurement.timestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
const levelMeasurement = levelChain.exists() ? levelChain.get() : null;
|
const levelMeasurement = levelChain.exists() ? levelChain.get() : null;
|
||||||
if (levelMeasurement) {
|
if (levelMeasurement) {
|
||||||
levelMeasurement.values = [];
|
levelMeasurement.values = [];
|
||||||
levelMeasurement.timestamps = [];
|
levelMeasurement.timestamps = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm');
|
levelChain.value(val, timestamp).unit(unit);
|
||||||
|
|
||||||
|
volumeChain.value(this._calcVolumeFromLevel(val), timestamp, 'm3');
|
||||||
|
|
||||||
this._predictedFlowState = {
|
this._predictedFlowState = {
|
||||||
inflow: 0,
|
inflow: 0,
|
||||||
@@ -374,7 +415,7 @@ class PumpingStation {
|
|||||||
if(this.safetyControllerActive) return;
|
if(this.safetyControllerActive) return;
|
||||||
|
|
||||||
//if safety not active proceed with normal control
|
//if safety not active proceed with normal control
|
||||||
this._controlLogic(snapshot,remaining.seconds);
|
this._controlLogic(snapshot,netFlow.direction);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
direction: netFlow.direction,
|
direction: netFlow.direction,
|
||||||
@@ -448,29 +489,50 @@ class PumpingStation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handler = (eventData = {}) => {
|
const handler = (eventData = {}) => {
|
||||||
const value = Number.isFinite(eventData.value) ? eventData.value : 0;
|
const flowUnit = this.measurements.getUnit('flow');
|
||||||
const timestamp = eventData.timestamp ?? Date.now();
|
const unit = eventData.unit || child.config?.general?.unit || flowUnit;
|
||||||
const unit = eventData.unit ?? 'm3/s';
|
const ts = eventData.timestamp || Date.now();
|
||||||
|
const posKeyBase = posKey; // 'in' or 'out'
|
||||||
|
|
||||||
this.logger.debug(`Predicted flow update from ${childName} (${childId}, ${posKey}) -> ${value} ${unit}`);
|
this.measurements
|
||||||
|
.type('flow')
|
||||||
|
.variant('predicted')
|
||||||
|
.position(posKeyBase)
|
||||||
|
.child(childId)
|
||||||
|
.value(eventData.value, ts, unit);
|
||||||
|
|
||||||
this.predictedFlowChildren.get(childId)[posKey] = value;
|
this._refreshAggregatedPredictedFlow();
|
||||||
this._refreshAggregatedPredictedFlow(posKey, timestamp, unit);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler));
|
eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') {
|
_refreshAggregatedPredictedFlow() {
|
||||||
const sum = Array.from(this.predictedFlowChildren.values())
|
const preferredUnit = this.measurements.getUnit('flow');
|
||||||
.map((entry) => (Number.isFinite(entry[direction]) ? entry[direction] : 0))
|
const childPositions = Object.keys(this.measurements.measurements?.flow?.predictedChild || {});
|
||||||
.reduce((acc, val) => acc + val, 0);
|
const inflowPositions = childPositions.filter((p) => p === 'in');
|
||||||
|
const outflowPositions = childPositions.filter((p) => p === 'out');
|
||||||
|
|
||||||
|
const sumIn = this.measurements.sum('flow', 'predicted', inflowPositions, preferredUnit);
|
||||||
|
const sumOut = this.measurements.sum('flow', 'predicted', outflowPositions, preferredUnit);
|
||||||
|
|
||||||
|
this.measurements.type('flow').variant('predicted').position('in').value(sumIn, Date.now(), preferredUnit);
|
||||||
|
this.measurements.type('flow').variant('predicted').position('out').value(sumOut, Date.now(), preferredUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
setManualInflow(value, timestamp = Date.now(), unit) {
|
||||||
|
const num = Number(value);
|
||||||
|
const unit = this.measurements.getUnit('flow');
|
||||||
|
|
||||||
|
// Write manual inflow into the aggregated bucket
|
||||||
this.measurements
|
this.measurements
|
||||||
.type('flow')
|
.type('flow')
|
||||||
.variant('predicted')
|
.variant('predicted')
|
||||||
.position(direction)
|
.position('in')
|
||||||
.value(sum, timestamp, unit);
|
.child('manual-qin')
|
||||||
|
.value(num, timestamp, unit);
|
||||||
|
|
||||||
|
this._refreshAggregatedPredictedFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMeasurement(measurementType, value, position, context) {
|
_handleMeasurement(measurementType, value, position, context) {
|
||||||
@@ -661,11 +723,12 @@ class PumpingStation {
|
|||||||
|
|
||||||
if (!flow.inflow.exists && !flow.outflow.exists) continue;
|
if (!flow.inflow.exists && !flow.outflow.exists) continue;
|
||||||
|
|
||||||
const inflow = flow.inflow.current?.value ?? 0;
|
const unit = this.measurements.getUnit('flow');
|
||||||
const outflow = flow.outflow.current?.value ?? 0;
|
const inflow = flow.inflow.current?.value ?? flow.inflow.previous?.value ?? 0;
|
||||||
const net = inflow - outflow; // positive => filling
|
const outflow = flow.outflow.current?.value ?? flow.outflow.previous?.value ?? 0;
|
||||||
|
const net = inflow - outflow; // -> pos is filling
|
||||||
|
|
||||||
this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net).unit('m3/s');
|
this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net, Date.now(), unit);
|
||||||
this.logger.debug(`inflow : ${inflow} - outflow : ${outflow}`);
|
this.logger.debug(`inflow : ${inflow} - outflow : ${outflow}`);
|
||||||
|
|
||||||
return { value: net,source: variant,direction: this._deriveDirection(net) };
|
return { value: net,source: variant,direction: this._deriveDirection(net) };
|
||||||
@@ -869,14 +932,8 @@ class PumpingStation {
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
getOutput() {
|
getOutput() {
|
||||||
const output = {};
|
|
||||||
Object.entries(this.measurements.measurements).forEach(([type, variants]) => {
|
const output = this.measurements.getFlattenedOutput();
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user