diff --git a/examples/02-Dashboard.json b/examples/02-Dashboard.json index ab52731..541a66a 100644 --- a/examples/02-Dashboard.json +++ b/examples/02-Dashboard.json @@ -166,8 +166,8 @@ "id": "b30af582f935bcb7", "type": "comment", "z": "77f00aef1c966167", - "name": "PumpingStation — Dashboard (Tier 2)", - "info": "Same command surface as the Basic example, driven by a FlowFuse dashboard.\n\nOpen /dashboard/pumpingstation-basic after deploy.\n\nCONTROLS panel\n- Mode buttons → set.mode (manual / levelbased)\n- Inflow / Outflow buttons → set.inflow / set.outflow (60 / 80 m³/h)\n- Demand button → set.demand (40 m³/h, manual mode only)\n- Calibrate buttons → cmd.calibrate.volume / cmd.calibrate.level\n\nSTATUS panel\n- 7 text rows: Mode, Direction, Level, Volume, Volume %, percControl, Manual demand\n\nTRENDS panel\n- 4 charts: Level (m), Volume (m³), Volume %, Flow (in/out/net m³/h)\n\nRAW OUTPUT panel\n- Full key/value dump of the latest Port 0 cache (sorted). Shows every field the node emits including basin geometry, safety thresholds, predicted overflow/underflow.\n\nThe fan-out function caches last-known values so delta-only Port 0 updates never blank a row.", + "name": "PumpingStation \u2014 Dashboard (Tier 2)", + "info": "Same command surface as the Basic example, driven by a FlowFuse dashboard.\n\nOpen /dashboard/pumpingstation-basic after deploy.\n\nCONTROLS panel\n- Mode buttons \u2192 set.mode (manual / levelbased)\n- Inflow / Outflow buttons \u2192 set.inflow / set.outflow (60 / 80 m\u00b3/h)\n- Demand button \u2192 set.demand (40 m\u00b3/h, manual mode only)\n- Calibrate buttons \u2192 cmd.calibrate.volume / cmd.calibrate.level\n\nSTATUS panel\n- 7 text rows: Mode, Direction, Level, Volume, Volume %, percControl, Manual demand\n\nTRENDS panel\n- 4 charts: Level (m), Volume (m\u00b3), Volume %, Flow (in/out/net m\u00b3/h)\n\nRAW OUTPUT panel\n- Full key/value dump of the latest Port 0 cache (sorted). Shows every field the node emits including basin geometry, safety thresholds, predicted overflow/underflow.\n\nThe fan-out function caches last-known values so delta-only Port 0 updates never blank a row.", "x": 660, "y": 320, "wires": [] @@ -332,13 +332,13 @@ "z": "77f00aef1c966167", "g": "a9f9b38b0e00c1d7", "group": "ui_group_ctrl", - "name": "Inflow 60 m³/h", - "label": "Inflow 60 m³/h", + "name": "Inflow 60 m\u00b3/h", + "label": "Inflow 60 m\u00b3/h", "order": 3, "width": "3", "height": "1", "emulateClick": false, - "tooltip": "Push a measured inflow of 60 m³/h into the basin balance", + "tooltip": "Push a measured inflow of 60 m\u00b3/h into the basin balance", "color": "", "bgcolor": "", "icon": "south", @@ -360,13 +360,13 @@ "z": "77f00aef1c966167", "g": "a9f9b38b0e00c1d7", "group": "ui_group_ctrl", - "name": "Outflow 80 m³/h", - "label": "Outflow 80 m³/h", + "name": "Outflow 80 m\u00b3/h", + "label": "Outflow 80 m\u00b3/h", "order": 4, "width": "3", "height": "1", "emulateClick": false, - "tooltip": "Push a measured outflow of 80 m³/h into the basin balance", + "tooltip": "Push a measured outflow of 80 m\u00b3/h into the basin balance", "color": "", "bgcolor": "", "icon": "north", @@ -388,13 +388,13 @@ "z": "77f00aef1c966167", "g": "42bf82c87d05f498", "group": "ui_group_ctrl", - "name": "Demand 40 m³/h", - "label": "Demand 40 m³/h (manual)", + "name": "Demand 40 m\u00b3/h", + "label": "Demand 40 m\u00b3/h (manual)", "order": 5, "width": "6", "height": "1", "emulateClick": false, - "tooltip": "Operator outflow demand — only forwarded when mode = manual", + "tooltip": "Operator outflow demand \u2014 only forwarded when mode = manual", "color": "", "bgcolor": "", "icon": "speed", @@ -416,13 +416,13 @@ "z": "77f00aef1c966167", "g": "234bdce20170061a", "group": "ui_group_ctrl", - "name": "Calibrate V=25 m³", - "label": "Calibrate V = 25 m³", + "name": "Calibrate V=25 m\u00b3", + "label": "Calibrate V = 25 m\u00b3", "order": 6, "width": "3", "height": "1", "emulateClick": false, - "tooltip": "Snap the predicted-volume integrator to 25 m³", + "tooltip": "Snap the predicted-volume integrator to 25 m\u00b3", "color": "", "bgcolor": "", "icon": "tune", @@ -472,8 +472,8 @@ "z": "77f00aef1c966167", "g": "grp_status_panel", "name": "fan-out Port 0 (status + charts + raw)", - "func": "// Port 0 emits delta-only — cache last-known so deltas never blank a row.\n// Keys with dots use the runtime childId (= node id), so we pattern-match\n// by prefix rather than hardcoding.\nconst cache = context.get('cache') || {};\nconst p = msg.payload || {};\nfor (const k in p) cache[k] = p[k];\ncontext.set('cache', cache);\n\nconst findByPrefix = (prefix) => {\n for (const k of Object.keys(cache)) if (k.startsWith(prefix)) return cache[k];\n return null;\n};\nconst num = (v, dp, unit) => {\n const n = +v;\n if (!Number.isFinite(n)) return '—';\n return n.toFixed(dp) + (unit ? ' ' + unit : '');\n};\n\nconst level = findByPrefix('level.predicted.atequipment.');\nconst volume = findByPrefix('volume.predicted.atequipment.');\nconst volPct = findByPrefix('volumePercent.predicted.atequipment.');\nconst qInS = findByPrefix('flow.predicted.in.');\nconst qOutS = findByPrefix('flow.predicted.out.');\nconst qNetS = findByPrefix('netFlowRate.predicted.atequipment.');\nconst qInH = Number.isFinite(+qInS) ? +qInS * 3600 : null;\nconst qOutH = Number.isFinite(+qOutS) ? +qOutS * 3600 : null;\nconst qNetH = Number.isFinite(+qNetS) ? +qNetS * 3600 : null;\nconst pct = cache.percControl;\nconst dem = cache.manualDemand;\nconst mode = cache.mode || '—';\nconst dir = cache.direction || '—';\n\nconst chart = (topic, v) => Number.isFinite(+v) ? { topic, payload: +v } : null;\n\n// Raw view: every cached key, sorted, with values prettified for display.\nconst rawRows = Object.keys(cache).sort().map((k) => {\n const v = cache[k];\n let display;\n if (v === null || v === undefined) display = '—';\n else if (typeof v === 'number') display = Number.isInteger(v) ? String(v) : v.toFixed(4);\n else display = String(v);\n return { key: k, value: display };\n});\n\nreturn [\n // 0–6: status text widgets\n { payload: mode },\n { payload: dir },\n { payload: num(level, 2, 'm') },\n { payload: num(volume, 2, 'm³') },\n { payload: num(volPct, 2, '%') },\n { payload: num(pct, 1, '%') },\n { payload: mode === 'manual'\n ? (Number.isFinite(+dem) ? num(dem, 1, 'm³/h') : 'not set')\n : '—' },\n // 7–9: single-series charts\n chart('Level', level),\n chart('Volume', volume),\n chart('Volume %', volPct),\n // 10–12: flow chart (three series share the same chart node)\n chart('Inflow', qInH),\n chart('Outflow', qOutH),\n chart('Net', qNetH),\n // 13: raw key/value rows for the ui-template\n { payload: rawRows },\n];\n", - "outputs": 14, + "func": "// Port 0 emits delta-only \u2014 cache last-known so deltas never blank a row.\n// Keys with dots use the runtime childId (= node id), so we pattern-match\n// by prefix rather than hardcoding.\nconst cache = context.get('cache') || {};\nconst p = msg.payload || {};\nfor (const k in p) cache[k] = p[k];\ncontext.set('cache', cache);\n\nconst findByPrefix = (prefix) => {\n for (const k of Object.keys(cache)) if (k.startsWith(prefix)) return cache[k];\n return null;\n};\nconst num = (v, dp, unit) => {\n const n = +v;\n if (!Number.isFinite(n)) return '\u2014';\n return n.toFixed(dp) + (unit ? ' ' + unit : '');\n};\n\nconst level = findByPrefix('level.predicted.atequipment.');\nconst volume = findByPrefix('volume.predicted.atequipment.');\nconst volPct = findByPrefix('volumePercent.predicted.atequipment.');\nconst qInS = findByPrefix('flow.predicted.in.');\nconst qOutS = findByPrefix('flow.predicted.out.');\nconst qNetS = findByPrefix('netFlowRate.predicted.atequipment.');\nconst qInH = Number.isFinite(+qInS) ? +qInS * 3600 : null;\nconst qOutH = Number.isFinite(+qOutS) ? +qOutS * 3600 : null;\nconst qNetH = Number.isFinite(+qNetS) ? +qNetS * 3600 : null;\nconst pct = cache.percControl;\nconst dem = cache.manualDemand;\nconst mode = cache.mode || '\u2014';\nconst dir = cache.direction || '\u2014';\n\nconst chart = (topic, v) => Number.isFinite(+v) ? { topic, payload: +v } : null;\n\n// Raw view: every cached key, sorted, with values prettified for display.\nconst rawRows = Object.keys(cache).sort().map((k) => {\n const v = cache[k];\n let display;\n if (v === null || v === undefined) display = '\u2014';\n else if (typeof v === 'number') display = Number.isInteger(v) ? String(v) : v.toFixed(4);\n else display = String(v);\n return { key: k, value: display };\n});\n\nreturn [\n // 0\u20136: status text widgets\n { payload: mode },\n { payload: dir },\n { payload: num(level, 2, 'm') },\n { payload: num(volume, 2, 'm\u00b3') },\n { payload: num(volPct, 2, '%') },\n { payload: num(pct, 1, '%') },\n { payload: mode === 'manual'\n ? (Number.isFinite(+dem) ? num(dem, 1, 'm\u00b3/h') : 'not set')\n : '\u2014' },\n // 7\u20139: single-series charts\n chart('Level', level),\n chart('Volume', volume),\n chart('Volume %', volPct),\n // 10\u201312: flow chart (three series share the same chart node)\n chart('Inflow', qInH),\n chart('Outflow', qOutH),\n chart('Net', qNetH),\n // 13: raw key/value rows for the ui-template\n { payload: rawRows },\n // 14: percControl chart\n chart('percControl', pct),\n];\n", + "outputs": 15, "timeout": 0, "noerr": 0, "initialize": "", @@ -523,6 +523,9 @@ ], [ "ui_tpl_raw" + ], + [ + "ui_chart_pumping_perccontrol" ] ] }, @@ -740,8 +743,8 @@ "z": "77f00aef1c966167", "g": "grp_status_panel", "group": "ui_group_trends", - "name": "Volume (m³)", - "label": "Volume (m³)", + "name": "Volume (m\u00b3)", + "label": "Volume (m\u00b3)", "order": 2, "width": 6, "height": 4, @@ -754,7 +757,7 @@ "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", - "yAxisLabel": "m³", + "yAxisLabel": "m\u00b3", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "xmin": "", @@ -862,8 +865,8 @@ "z": "77f00aef1c966167", "g": "grp_status_panel", "group": "ui_group_trends", - "name": "Flow (m³/h)", - "label": "Flow (m³/h) — Inflow / Outflow / Net", + "name": "Flow (m\u00b3/h)", + "label": "Flow (m\u00b3/h) \u2014 Inflow / Outflow / Net", "order": 4, "width": 6, "height": 4, @@ -876,7 +879,7 @@ "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", - "yAxisLabel": "m³/h", + "yAxisLabel": "m\u00b3/h", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "xmin": "", @@ -1029,7 +1032,7 @@ "enableLog": false, "logLevel": "error", "positionVsParent": "atEquipment", - "positionIcon": "⊥", + "positionIcon": "\u22a5", "hasDistance": false, "distance": "", "controlMode": "levelbased", @@ -1066,5 +1069,68 @@ "modules": { "EVOLV": "1.0.29" } + }, + { + "id": "ui_chart_pumping_perccontrol", + "type": "ui-chart", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "percControl", + "label": "percControl (%) \u2014 pumping-station demand", + "order": 5, + "width": 6, + "height": 4, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "time", + "xAxisType": "time", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "%", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "xmin": "", + "xmax": "", + "ymin": "0", + "ymax": "100", + "removeOlder": "15", + "removeOlderUnit": "60", + "removeOlderPoints": "", + "bins": 10, + "action": "append", + "stackSeries": false, + "pointShape": "circle", + "pointRadius": 4, + "interpolation": "linear", + "showLegend": false, + "className": "", + "colors": [ + "#A347E1", + "#FF0000", + "#FF7F0E", + "#2CA02C", + "#0095FF", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "x": 1240, + "y": 560, + "wires": [ + [] + ] } -] +] \ No newline at end of file