diff --git a/examples/basic-dashboard.flow.json b/examples/basic-dashboard.flow.json
new file mode 100644
index 0000000..7323bce
--- /dev/null
+++ b/examples/basic-dashboard.flow.json
@@ -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": "const 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', 'level.measured.atequipment');\nconst volume = firstFinite('volume.predicted.atequipment', 'volume.measured.atequipment');\nconst netFlow = firstFinite('netFlowRate.predicted.atequipment', 'netFlowRate.measured.atequipment');\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": []
+ }
+]
diff --git a/pumpingStation.html b/pumpingStation.html
index 216f660..92f664b 100644
--- a/pumpingStation.html
+++ b/pumpingStation.html
@@ -8,8 +8,17 @@
| **Control Module** | `#a9daee` | zwart |
-->
-
-
+
+
+
+
+
+
+
+
+
+