Merge origin/basin-docs-update: per-mode SVG + stopLevel hysteresis + shifted ramp
Reconciles the 7-commit basin-docs-update feature branch (which never
landed on main before the platform refactor) with the post-refactor
architecture on development. Each basin-docs feature ported into the
relevant concern module:
control/levelBased.js
- stopLevel Schmitt-trigger + dead-band keep-alive
- Shifted ramp (arm % → hold @ 100% → ramp down to shiftLevel)
- Linear vs log up-curve (curveType + logCurveFactor)
measurement/flowAggregator.js
- Predicted-volume overflow clamp + spill flow stream
- Cumulative overflowVolume + underflowVolume
- Hard floor at 0 + dry-run-on-transition handling
basin/thresholdValidator.js
- computeSafetyPoints exposes dryRunLevel + highVolumeSafetyLevel
- startLevel ≤ inflowLevel invariant added
measurement/calibration.js + commands/
- Manual q_out path (set.outflow / q_out alias)
safety/safetyController.js
- Accepts both legacy + new high-volume threshold names
UI:
pumpingStation.html — restored the side-panel + SVG mode-preview block,
added defaults for stopLevel/shiftLevel/shiftArmPercent/levelCurveType/
logCurveFactor/enableShiftedRamp.
src/editor/* — basin-docs' 7-file modular editor (replaces single
src/editor.js, which is deleted).
pumpingStation.js — admin endpoint serves editor/:file.
Tests: 130/130 pass (125 basic + 5 integration). Two basin-docs test
files added: nodeClass-config.test.js, basic-dashboard-flow.test.js,
shifted-ramp-end-to-end.test.js. One pre-refactor control-levelBased
test adapted to match basin-docs canonical "no-shutdown in dead zone"
behaviour.
Human-review items (see commit context):
- rampFoot = inflowLevel (matches basin-docs test); basin-docs source
used rampFoot = startLevel. Domain owner: confirm intent.
- Naming kept dual (overfillLevel + highVolumeSafetyLevel).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
589
examples/basic-dashboard.flow.json
Normal file
589
examples/basic-dashboard.flow.json
Normal file
@@ -0,0 +1,589 @@
|
||||
[
|
||||
{
|
||||
"id": "ps_tab_basic_dashboard",
|
||||
"type": "tab",
|
||||
"label": "PumpingStation Dashboard",
|
||||
"disabled": false,
|
||||
"info": "Basic level-based pumpingStation dashboard with basin trends and safety state."
|
||||
},
|
||||
{
|
||||
"id": "ui_base_ps_basic",
|
||||
"type": "ui-base",
|
||||
"name": "EVOLV Demo",
|
||||
"path": "/dashboard",
|
||||
"appIcon": "",
|
||||
"includeClientData": true,
|
||||
"acceptsClientConfig": [
|
||||
"ui-notification",
|
||||
"ui-control"
|
||||
],
|
||||
"showPathInSidebar": false,
|
||||
"headerContent": "page",
|
||||
"navigationStyle": "default",
|
||||
"titleBarStyle": "default"
|
||||
},
|
||||
{
|
||||
"id": "ui_theme_ps_basic",
|
||||
"type": "ui-theme",
|
||||
"name": "EVOLV Pumping Theme",
|
||||
"colors": {
|
||||
"surface": "#ffffff",
|
||||
"primary": "#0c99d9",
|
||||
"bgPage": "#f1f3f5",
|
||||
"groupBg": "#ffffff",
|
||||
"groupOutline": "#cfd7de"
|
||||
},
|
||||
"sizes": {
|
||||
"density": "default",
|
||||
"pagePadding": "14px",
|
||||
"groupGap": "14px",
|
||||
"groupBorderRadius": "6px",
|
||||
"widgetGap": "12px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui_page_ps_basic",
|
||||
"type": "ui-page",
|
||||
"name": "PumpingStation",
|
||||
"ui": "ui_base_ps_basic",
|
||||
"path": "/pumping-station",
|
||||
"icon": "water_drop",
|
||||
"layout": "grid",
|
||||
"theme": "ui_theme_ps_basic",
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "Default",
|
||||
"px": "0",
|
||||
"cols": "12"
|
||||
}
|
||||
],
|
||||
"order": 1,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_ps_inputs",
|
||||
"type": "ui-group",
|
||||
"name": "Simulation Inputs",
|
||||
"page": "ui_page_ps_basic",
|
||||
"width": "4",
|
||||
"height": "1",
|
||||
"order": 1,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_ps_trends",
|
||||
"type": "ui-group",
|
||||
"name": "Basin Trends",
|
||||
"page": "ui_page_ps_basic",
|
||||
"width": "8",
|
||||
"height": "1",
|
||||
"order": 2,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_ps_state",
|
||||
"type": "ui-group",
|
||||
"name": "State",
|
||||
"page": "ui_page_ps_basic",
|
||||
"width": "12",
|
||||
"height": "1",
|
||||
"order": 3,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ps_node_basic",
|
||||
"type": "pumpingStation",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "PS Dashboard Demo",
|
||||
"basinVolume": 50,
|
||||
"basinHeight": 5,
|
||||
"inflowLevel": 3,
|
||||
"outflowLevel": 0.2,
|
||||
"overflowLevel": 4.5,
|
||||
"defaultFluid": "wastewater",
|
||||
"inletPipeDiameter": 0.4,
|
||||
"outletPipeDiameter": 0.3,
|
||||
"pipelineLength": 80,
|
||||
"maxDischargeHead": 24,
|
||||
"staticHead": 12,
|
||||
"maxInflowRate": 200,
|
||||
"temperatureReferenceDegC": 15,
|
||||
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||
"enableDryRunProtection": true,
|
||||
"enableHighVolumeSafety": true,
|
||||
"enableOverfillProtection": true,
|
||||
"dryRunThresholdPercent": 2,
|
||||
"highVolumeSafetyThresholdPercent": 98,
|
||||
"overfillThresholdPercent": 98,
|
||||
"minHeightBasedOn": "outlet",
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"refHeight": "NAP",
|
||||
"basinBottomRef": 0,
|
||||
"unit": "m3/h",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": 0,
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"controlMode": "levelbased",
|
||||
"levelCurveType": "linear",
|
||||
"logCurveFactor": 9,
|
||||
"minLevel": 1,
|
||||
"startLevel": 2,
|
||||
"maxLevel": 4,
|
||||
"x": 720,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"ps_parse_output"
|
||||
],
|
||||
[
|
||||
"ps_debug_influx"
|
||||
],
|
||||
[
|
||||
"ps_debug_parent"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_calibrate_initial",
|
||||
"type": "inject",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Set start level 2 m",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "0.5",
|
||||
"topic": "calibratePredictedLevel",
|
||||
"payload": "2",
|
||||
"payloadType": "num",
|
||||
"x": 180,
|
||||
"y": 180,
|
||||
"wires": [
|
||||
[
|
||||
"ps_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_auto_inflow",
|
||||
"type": "inject",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Auto inflow 0.008 m3/s",
|
||||
"props": [
|
||||
{
|
||||
"p": "payload"
|
||||
}
|
||||
],
|
||||
"repeat": "1",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "1",
|
||||
"topic": "",
|
||||
"payload": "0.008",
|
||||
"payloadType": "num",
|
||||
"x": 180,
|
||||
"y": 240,
|
||||
"wires": [
|
||||
[
|
||||
"ps_build_qin"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_inflow_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_inputs",
|
||||
"name": "Inflow",
|
||||
"label": "Inflow (m3/s)",
|
||||
"order": 1,
|
||||
"width": "4",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 0.05,
|
||||
"step": 0.001,
|
||||
"x": 190,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"ps_build_qin"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_build_qin",
|
||||
"type": "function",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Build q_in",
|
||||
"func": "msg.topic = 'q_in';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 440,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"ps_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_outflow_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_inputs",
|
||||
"name": "Outflow",
|
||||
"label": "Outflow (m3/s)",
|
||||
"order": 2,
|
||||
"width": "4",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 0.05,
|
||||
"step": 0.001,
|
||||
"x": 190,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_build_qout"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_build_qout",
|
||||
"type": "function",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Build q_out",
|
||||
"func": "msg.topic = 'q_out';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 440,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_parse_output",
|
||||
"type": "function",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Parse PS output",
|
||||
"func": "// MeasurementContainer flat keys are `${type}.${variant}.${position}.${childId}`.\n// When PS writes without an explicit .child(), the childId is the literal\n// string 'default' — DON'T strip it. See generalFunctions/src/measurements/\n// MeasurementContainer.js getFlattenedOutput for details.\nconst fields = (msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst snapshot = Object.assign({}, context.get('snapshot') || {}, fields);\ncontext.set('snapshot', snapshot);\nconst firstFinite = (...keys) => {\n for (const key of keys) {\n const value = Number(snapshot[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\nconst level = firstFinite('level.predicted.atequipment.default', 'level.measured.atequipment.default');\nconst volume = firstFinite('volume.predicted.atequipment.default', 'volume.measured.atequipment.default');\nconst netFlow = firstFinite('netFlowRate.predicted.atequipment.default', 'netFlowRate.measured.atequipment.default');\nconst demand = firstFinite('percControl');\nconst safety = snapshot.safetyState || 'normal';\nconst direction = snapshot.direction || 'unknown';\nconst overflow = snapshot.isOverflowing === true || snapshot.isOverflowing === 'true';\nconst timeleft = Number(snapshot.timeleft);\nconst fmt = (value, digits = 2) => Number.isFinite(value) ? value.toFixed(digits) : '-';\nreturn [\n level == null ? null : { topic: 'level', payload: level },\n volume == null ? null : { topic: 'volume', payload: volume },\n demand == null ? null : { topic: 'demand', payload: demand },\n netFlow == null ? null : { topic: 'net_flow', payload: netFlow },\n { topic: 'safety', payload: `${safety} | overflowing=${overflow}` },\n { topic: 'snapshot', payload: `level=${fmt(level)} m | volume=${fmt(volume)} m3 | demand=${fmt(demand, 0)}% | direction=${direction} | t=${Number.isFinite(timeleft) ? Math.round(timeleft) + ' s' : '-'}` }\n];",
|
||||
"outputs": 6,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 980,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"ps_chart_level"
|
||||
],
|
||||
[
|
||||
"ps_chart_volume"
|
||||
],
|
||||
[
|
||||
"ps_chart_demand"
|
||||
],
|
||||
[
|
||||
"ps_chart_netflow"
|
||||
],
|
||||
[
|
||||
"ps_text_safety"
|
||||
],
|
||||
[
|
||||
"ps_text_snapshot"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_level",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Level",
|
||||
"label": "Level (m)",
|
||||
"order": 1,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1230,
|
||||
"y": 140,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "0",
|
||||
"ymax": "5",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#0c99d9"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_volume",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Volume",
|
||||
"label": "Volume (m3)",
|
||||
"order": 2,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m3",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1230,
|
||||
"y": 200,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "0",
|
||||
"ymax": "50",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#2ca02c"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_demand",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Demand",
|
||||
"label": "Demand (%)",
|
||||
"order": 3,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "%",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1230,
|
||||
"y": 260,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "0",
|
||||
"ymax": "120",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#d68910"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_netflow",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Net Flow",
|
||||
"label": "Net flow (m3/s)",
|
||||
"order": 4,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m3/s",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1240,
|
||||
"y": 320,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "",
|
||||
"ymax": "",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#9467bd"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_text_safety",
|
||||
"type": "ui-text",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_state",
|
||||
"name": "Safety",
|
||||
"label": "Safety",
|
||||
"order": 1,
|
||||
"width": 4,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1230,
|
||||
"y": 380,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_text_snapshot",
|
||||
"type": "ui-text",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_state",
|
||||
"name": "Snapshot",
|
||||
"label": "Snapshot",
|
||||
"order": 2,
|
||||
"width": 8,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1240,
|
||||
"y": 440,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_debug_influx",
|
||||
"type": "debug",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Influx output",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 980,
|
||||
"y": 320,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_debug_parent",
|
||||
"type": "debug",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Parent output",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 980,
|
||||
"y": 380,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user