Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.
src/basin/ BasinGeometry + thresholdValidator (pure)
src/measurement/ flowAggregator + measurementRouter + calibration
src/control/ levelBased + flowBased(stub) + manual + index dispatcher
src/safety/ safetyController split into dryRun + overfill rules
src/commands/ registry array + handlers (canonical names from start)
src/editor.js 260 lines of SVG basin-diagram redraw, was inline in .html
examples/standalone-demo.js was if(require.main===module) at bottom of specificClass.js
CONTRACT.md canonical inputs + outputs + emitted events
Modified:
src/specificClass.js removed the 170-line standalone demo block
pumpingStation.html oneditprepare/oneditsave delegate to editor.{init,save}
pumpingStation.js added admin endpoint serving src/editor.js
102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
354 lines
17 KiB
HTML
354 lines
17 KiB
HTML
<!--
|
|
| S88-niveau | Primair (blokkleur) | Tekstkleur |
|
|
| ---------------------- | ------------------- | ---------- |
|
|
| **Area** | `#0f52a5` | wit |
|
|
| **Process Cell** | `#0c99d9` | wit |
|
|
| **Unit** | `#50a8d9` | zwart |
|
|
| **Equipment (Module)** | `#86bbdd` | zwart |
|
|
| **Control Module** | `#a9daee` | zwart |
|
|
|
|
-->
|
|
<script src="/pumpingStation/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
|
|
<script src="/pumpingStation/configData.js"></script> <!-- Load the config script for node information -->
|
|
<script src="/pumpingStation/editor.js"></script> <!-- Load the basin-diagram editor logic -->
|
|
|
|
<script>//test
|
|
RED.nodes.registerType("pumpingStation", {
|
|
category: "EVOLV",
|
|
color: "#0c99d9", // color for the node based on the S88 schema
|
|
defaults: {
|
|
name: { value: "" },
|
|
|
|
// Define station-specific properties
|
|
simulator: { value: false },
|
|
basinVolume: { value: 1 }, // m³, total empty basin
|
|
basinHeight: { value: 1 }, // m, floor to top
|
|
inflowLevel: { value: 0.8 }, // m, centre of inlet pipe above floor
|
|
outflowLevel: { value: 0.2 }, // m, centre of outlet pipe above floor
|
|
overflowLevel: { value: 0.9 }, // m, overflow elevation
|
|
defaultFluid: { value: "wastewater" },
|
|
inletPipeDiameter: { value: 0.3 }, // m
|
|
outletPipeDiameter: { value: 0.3 }, // m
|
|
pipelineLength: { value: 80 }, // m
|
|
maxDischargeHead: { value: 24 }, // m
|
|
staticHead: { value: 12 }, // m
|
|
maxInflowRate: { value: 200 }, // m³/h
|
|
temperatureReferenceDegC: { value: 15 },
|
|
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
|
|
processOutputFormat: { value: "process" },
|
|
dbaseOutputFormat: { value: "influxdb" },
|
|
|
|
// Advanced reference information
|
|
refHeight: { value: "NAP" }, // reference height
|
|
basinBottomRef: { value: 1 }, // absolute elevation of basin floor
|
|
|
|
//define asset properties
|
|
uuid: { value: "" },
|
|
supplier: { value: "" },
|
|
category: { value: "" },
|
|
assetType: { value: "" },
|
|
model: { value: "" },
|
|
unit: { value: "" },
|
|
|
|
//logger properties
|
|
enableLog: { value: false },
|
|
logLevel: { value: "error" },
|
|
|
|
//physicalAspect
|
|
positionVsParent: { value: "" },
|
|
positionIcon: { value: "" },
|
|
hasDistance: { value: false },
|
|
distance: { value: 0 },
|
|
distanceUnit: { value: "m" },
|
|
distanceDescription: { value: "" },
|
|
|
|
// control strategy
|
|
controlMode: { value: "none" },
|
|
startLevel: { value: null },
|
|
minLevel: { value: null },
|
|
maxLevel: { value: null },
|
|
flowSetpoint: { value: null },
|
|
flowDeadband: { value: null }
|
|
|
|
},
|
|
|
|
inputs: 1,
|
|
outputs: 3,
|
|
inputLabels: ["Input"],
|
|
outputLabels: ["process", "dbase", "parent"],
|
|
icon: "font-awesome/fa-tint",
|
|
|
|
label: function () {
|
|
return this.positionIcon + " PumpingStation";
|
|
},
|
|
|
|
oneditprepare: function () {
|
|
window.EVOLV?.nodes?.pumpingStation?.editor?.init(this);
|
|
},
|
|
oneditsave: function () {
|
|
const node = this;
|
|
window.EVOLV?.nodes?.pumpingStation?.loggerMenu?.saveEditor?.(node);
|
|
window.EVOLV?.nodes?.pumpingStation?.positionMenu?.saveEditor?.(node);
|
|
window.EVOLV?.nodes?.pumpingStation?.editor?.save(node);
|
|
},
|
|
|
|
});
|
|
</script>
|
|
|
|
<!-- Main UI -->
|
|
|
|
<script type="text/html" data-template-name="pumpingStation">
|
|
|
|
<h4>Simulation</h4>
|
|
<div class="form-row">
|
|
<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;" />
|
|
<span>Run station in simulated mode</span>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h4>Basin parameters</h4>
|
|
<p style="font-size:12px;color:#777;margin:0 0 8px 0;">Heights are measured from the basin floor (0 m). Enter values next to each line — the diagram scales to whatever you enter.</p>
|
|
|
|
<style>
|
|
#ps-basin-diagram input[type=number] {
|
|
width: 100%; height: 20px; box-sizing: border-box;
|
|
font-size: 11px; padding: 1px 4px; margin: 0;
|
|
border: 1px solid #ccc; border-radius: 3px; background: #fff;
|
|
}
|
|
#ps-basin-diagram input[type=number]:focus { outline: 1px solid #0c99d9; border-color: #0c99d9; }
|
|
</style>
|
|
|
|
<svg id="ps-basin-diagram" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 430"
|
|
style="display:block;width:100%;max-width:540px;margin:0 0 12px 0;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
|
|
font-family="Arial,sans-serif" font-size="11">
|
|
<defs>
|
|
<marker id="ps-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1F4E79" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- Tank body -->
|
|
<rect x="200" y="40" width="120" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
|
|
<!-- Dead-volume band (y + height updated dynamically below outflowLevel) -->
|
|
<rect id="ps-deadvol" x="201" width="118" fill="#AACCE0" />
|
|
<!-- basinVolume — pinned above the rim -->
|
|
<text id="ps-label-basinVolume" x="330" y="19" fill="#333" font-weight="600">basin volume</text>
|
|
<foreignObject id="ps-fo-basinVolume" x="425" y="4" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-basinVolume" min="0" step="0.1" />
|
|
</foreignObject>
|
|
<text id="ps-unit-basinVolume" x="500" y="19" fill="#555">m³</text>
|
|
|
|
<!-- Zone labels (mid-tank italic, positioned dynamically at midpoint between adjacent thresholds) -->
|
|
<text id="ps-zone-spare" x="260" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare volume before spilling</text>
|
|
<text id="ps-zone-sewage" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + tank buffer</text>
|
|
<text id="ps-zone-buffer1" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
|
|
<text id="ps-zone-buffer2" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
|
|
<text id="ps-zone-dead" x="260" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead volume</text>
|
|
|
|
|
|
<!-- basinHeight — always at tank rim (y=40 in viewBox coords) -->
|
|
<line id="ps-line-basinHeight" x1="195" y1="40" x2="325" y2="40" stroke="#333" stroke-width="1.5" />
|
|
<text id="ps-label-basinHeight" x="330" y="44" fill="#333">basinHeight</text>
|
|
<foreignObject id="ps-fo-basinHeight" x="425" y="29" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-basinHeight" min="0" step="0.1" />
|
|
</foreignObject>
|
|
<text id="ps-unit-basinHeight" x="500" y="44" fill="#555">m</text>
|
|
|
|
<!-- overflowLevel -->
|
|
<line id="ps-line-overflowLevel" x1="195" x2="325" stroke="#C0392B" stroke-dasharray="4 2" stroke-width="1.5" />
|
|
<text id="ps-label-overflowLevel" x="330" fill="#C0392B">overflowLevel</text>
|
|
<foreignObject id="ps-fo-overflowLevel" x="425" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-overflowLevel" min="0" step="0.01" />
|
|
</foreignObject>
|
|
<text id="ps-unit-overflowLevel" x="500" fill="#555">m</text>
|
|
|
|
<!-- maxLevel -->
|
|
<line id="ps-line-maxLevel" x1="195" x2="325" stroke="#D68910" stroke-dasharray="4 2" stroke-width="1.5" />
|
|
<text id="ps-label-maxLevel" x="330" fill="#D68910">maxLevel</text>
|
|
<foreignObject id="ps-fo-maxLevel" x="425" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-maxLevel" min="0" step="0.01" />
|
|
</foreignObject>
|
|
<text id="ps-unit-maxLevel" x="500" fill="#555">m</text>
|
|
|
|
<!-- startLevel -->
|
|
<line id="ps-line-startLevel" x1="195" x2="325" stroke="#1E8449" stroke-dasharray="4 2" stroke-width="1.5" />
|
|
<text id="ps-label-startLevel" x="330" fill="#1E8449">startLevel</text>
|
|
<foreignObject id="ps-fo-startLevel" x="425" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-startLevel" min="0" step="0.01" />
|
|
</foreignObject>
|
|
<text id="ps-unit-startLevel" x="500" fill="#555">m</text>
|
|
|
|
<!-- Inlet — arrow + input on the left -->
|
|
<line id="ps-line-inflowLevel" x1="140" x2="200" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
|
<text id="ps-label-inflowLevel" x="135" text-anchor="end" fill="#1F4E79" font-weight="bold">Inlet</text>
|
|
<text id="ps-sub-inflowLevel" x="135" text-anchor="end" fill="#777" font-size="9">bottom of pipe</text>
|
|
<foreignObject id="ps-fo-inflowLevel" x="5" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-inflowLevel" min="0" step="0.01" />
|
|
</foreignObject>
|
|
<text id="ps-unit-inflowLevel" x="80" fill="#555">m</text>
|
|
|
|
<!-- minLevel -->
|
|
<line id="ps-line-minLevel" x1="195" x2="325" stroke="#6C3483" stroke-dasharray="4 2" stroke-width="1.5" />
|
|
<text id="ps-label-minLevel" x="330" fill="#6C3483">minLevel</text>
|
|
<foreignObject id="ps-fo-minLevel" x="425" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-minLevel" min="0" step="0.01" />
|
|
</foreignObject>
|
|
<text id="ps-unit-minLevel" x="500" fill="#555">m</text>
|
|
|
|
<!-- dryRunLevel (derived, read-only) -->
|
|
<line id="ps-line-dryRunLevel" x1="195" x2="325" stroke="#C0392B" stroke-dasharray="1 2" stroke-width="1" opacity="0.6" />
|
|
<text id="ps-label-dryRunLevel" x="330" fill="#C0392B" font-size="10" font-style="italic">dryRunLevel ≈ — m (safety — from %)</text>
|
|
|
|
<!-- Outlet — arrow on right, input below the threshold column -->
|
|
<line id="ps-line-outflowLevel" x1="320" x2="360" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
|
<text id="ps-label-outflowLevel" x="365" fill="#1F4E79" font-weight="bold">Outlet</text>
|
|
<text id="ps-sub-outflowLevel" x="365" fill="#777" font-size="9">top of pipe</text>
|
|
<foreignObject id="ps-fo-outflowLevel" x="425" width="70" height="22">
|
|
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-outflowLevel" min="0" step="0.01" />
|
|
</foreignObject>
|
|
<text id="ps-unit-outflowLevel" x="500" fill="#555">m</text>
|
|
|
|
<!-- Floor / datum -->
|
|
<line x1="195" y1="380" x2="325" y2="380" stroke="#000" stroke-width="2" />
|
|
<text x="330" y="384" fill="#000">0 m (datum)</text>
|
|
|
|
<!-- Leader lines: shown when the input row had to be nudged off its threshold's ideal y -->
|
|
<line id="ps-leader-basinHeight" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
<line id="ps-leader-overflowLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
<line id="ps-leader-maxLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
<line id="ps-leader-startLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
<line id="ps-leader-minLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
<line id="ps-leader-dryRunLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
<line id="ps-leader-outflowLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
|
|
|
<!-- Ordering-warning ribbon -->
|
|
<text id="ps-warning" x="260" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
|
|
</svg>
|
|
|
|
<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">
|
|
<p style="font-size:12px;color:#777;margin:0;">Level-based uses <code>minLevel</code> / <code>startLevel</code> / <code>maxLevel</code> from the diagram above.</p>
|
|
</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 -->
|
|
<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">
|
|
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
|
|
<select id="node-input-refHeight" style="width:60%;">
|
|
<option value="NAP">NAP</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label for="node-input-basinBottomRef"><i class="fa fa-level-down"></i> Basin floor above datum (m)</label>
|
|
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
|
</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" style="width:80px;" />
|
|
<span id="derived-dryRunLevel" style="margin-left:8px;color:#777;font-size:12px;">→ dryRunLevel ≈ — m</span>
|
|
</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" style="width:80px;" />
|
|
<span id="derived-overfillLevel" style="margin-left:8px;color:#777;font-size:12px;">→ overfillLevel ≈ — m</span>
|
|
</div>
|
|
<hr>
|
|
|
|
<h3>Output Formats</h3>
|
|
<div class="form-row">
|
|
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
|
|
<select id="node-input-processOutputFormat" style="width:60%;">
|
|
<option value="process">process</option>
|
|
<option value="json">json</option>
|
|
<option value="csv">csv</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row">
|
|
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
|
|
<select id="node-input-dbaseOutputFormat" style="width:60%;">
|
|
<option value="influxdb">influxdb</option>
|
|
<option value="json">json</option>
|
|
<option value="csv">csv</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Shared asset/logger/position menus -->
|
|
<div id="asset-fields-placeholder"></div>
|
|
<div id="logger-fields-placeholder"></div>
|
|
<div id="position-fields-placeholder"></div>
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<script type="text/html" data-help-name="pumpingStation">
|
|
|
|
|
|
</script>
|