From 7aacee648290146ce3c8be62f23a7cbe43ecc9af Mon Sep 17 00:00:00 2001 From: znetsixe Date: Mon, 13 Apr 2026 15:53:47 +0200 Subject: [PATCH] feat(examples): pumpingstation-3pumps-dashboard end-to-end demo + bump generalFunctions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New top-level examples/ folder for end-to-end demos that show how multiple EVOLV nodes work together (complementing the per-node example flows under nodes//examples/). Future end-to-end demos will live as siblings. First demo: pumpingstation-3pumps-dashboard - 1 pumpingStation (basin model, manual mode for the demo so it observes rather than auto-shutting pumps; safety guards disabled — see README) - 1 machineGroupControl (optimalcontrol mode, absolute scaling) - 3 rotatingMachine pumps (hidrostal-H05K-S03R curve) - 6 measurement nodes (per pump: upstream + downstream pressure mbar, simulator mode for continuous activity) - Process demand input via dashboard slider (0-300 m3/h) AND auto random generator (3s tick, [40, 240] m3/h) — both feed PS q_in + MGC Qd - Auto/Manual mode toggle (broadcasts setMode to all 3 pumps) - Station-wide Start / Stop / Emergency-Stop buttons - Per-pump setpoint slider, individual buttons, full status text - Two trend charts (flow per pump, power per pump) - FlowFuse dashboard at /dashboard/pumping-station-demo build_flow.py is the source of truth — it generates flow.json deterministically and is the right place to extend the demo. Bumps: nodes/generalFunctions 43f6906 -> 29b78a3 Fix: childRegistrationUtils now aliases the production softwareType values (rotatingmachine, machinegroupcontrol) to the dispatch keys parent nodes check for (machine, machinegroup). Without this, MGC <-> rotatingMachine and pumpingStation <-> MGC wiring silently never matched in production even though tests passed. Demo confirms: MGC reports '3 machine(s) connected'. Verified end-to-end on Dockerized Node-RED 2026-04-13: pumps reach operational ~5s after deploy, MGC distributes random demand across them, basin tracks net flow direction, all dashboard widgets update each second. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/README.md | 42 + .../pumpingstation-3pumps-dashboard/README.md | 112 + .../build_flow.py | 766 ++++++ .../pumpingstation-3pumps-dashboard/flow.json | 2369 +++++++++++++++++ nodes/generalFunctions | 2 +- 5 files changed, 3290 insertions(+), 1 deletion(-) create mode 100644 examples/README.md create mode 100644 examples/pumpingstation-3pumps-dashboard/README.md create mode 100644 examples/pumpingstation-3pumps-dashboard/build_flow.py create mode 100644 examples/pumpingstation-3pumps-dashboard/flow.json diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..0af6073 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,42 @@ +# EVOLV — End-to-End Example Flows + +Demo flows that show how multiple EVOLV nodes work together in a realistic wastewater-automation scenario. Each example is self-contained: its folder has a `flow.json` you can import directly into Node-RED plus a `README.md` that walks through the topology, control modes, and dashboard layout. + +These flows complement the per-node example flows under `nodes//examples/` (which exercise a single node in isolation). Use the per-node flows for smoke tests during development; use the flows here when you want to see how a real plant section behaves end-to-end. + +## Catalogue + +| Folder | What it shows | +|---|---| +| [`pumpingstation-3pumps-dashboard/`](pumpingstation-3pumps-dashboard/) | Wet-well basin + machineGroupControl orchestrating 3 pumps (each with up/downstream pressure measurements), individual + auto control, process-demand input via dashboard slider or random generator, full FlowFuse dashboard. | + +## How to import + +1. Bring up the EVOLV stack: `docker compose up -d` from the superproject root. +2. Open Node-RED at `http://localhost:1880`. +3. Menu → **Import** → drop in the example's `flow.json` (or paste the contents). +4. Open the FlowFuse dashboard at `http://localhost:1880/dashboard`. + +Each example uses a unique dashboard `path` so they can coexist in the same Node-RED runtime. + +## Adding new examples + +When you create a new end-to-end example: + +1. Make a subfolder under `examples/` named `-`. +2. Include `flow.json` (Node-RED export) and `README.md` (topology, control modes, dashboard map, things to try). +3. Test it on a fresh Dockerized Node-RED — clean import, no errors, dashboard loads. +4. Add a row to the catalogue table above. + +## Wishlist for future examples + +These are scenarios worth building when there's a session for it: + +- **Pump failure + MGC re-routing** — kill pump 2 mid-run, watch MGC redistribute to pumps 1 and 3. +- **Energy-optimal vs equal-flow control** — same demand profile run through `optimalcontrol` and `prioritycontrol` modes side-by-side, energy comparison chart. +- **Schedule-driven demand** — diurnal flow pattern (low at night, peak at 7 am), MGC auto-tuning over 24 simulated hours. +- **Reactor + clarifier loop** — `reactor` upstream feeding `settler`, return sludge controlled by a small `pumpingStation`. +- **Diffuser + DO control** — aeration grid driven by a PID controller from a dissolved-oxygen sensor. +- **Digital sensor bundle** — MQTT-style sensor (BME280, ATAS, etc.) feeding a `measurement` node in digital mode + parent equipment node. +- **Maintenance window** — entermaintenance / exitmaintenance cycle with operator handover dashboard. +- **Calibration walk-through** — measurement node calibrate cycle with stable / unstable input demonstrations. diff --git a/examples/pumpingstation-3pumps-dashboard/README.md b/examples/pumpingstation-3pumps-dashboard/README.md new file mode 100644 index 0000000..1538868 --- /dev/null +++ b/examples/pumpingstation-3pumps-dashboard/README.md @@ -0,0 +1,112 @@ +# Pumping Station — 3 Pumps with Dashboard + +A complete end-to-end EVOLV stack: a wet-well basin model, a `machineGroupControl` orchestrating three `rotatingMachine` pumps (each with upstream/downstream pressure measurements), process-demand input from either a dashboard slider or an auto random generator, individual + auto control modes, and a FlowFuse dashboard with status, gauges, and trend charts. + +This is the canonical "make sure everything works together" demo for the platform. Use it after any cross-node refactor to confirm the architecture still hangs together end-to-end. + +## Quick start + +```bash +cd /mnt/d/gitea/EVOLV +docker compose up -d +# Wait for http://localhost:1880/nodes to return 200, then: +curl -s -X POST http://localhost:1880/flows \ + -H "Content-Type: application/json" \ + -H "Node-RED-Deployment-Type: full" \ + --data-binary @examples/pumpingstation-3pumps-dashboard/flow.json +``` + +Or open Node-RED at , **Import → drop the `flow.json`**, click **Deploy**. + +Then open the dashboard: + +- + +## What the flow contains + +| Layer | Node(s) | Role | +|---|---|---| +| Top | `pumpingStation` "Pumping Station" | Wet-well basin model. Tracks inflow (`q_in`), outflow (from machine-group child predictions), basin level/volume. PS is in `manual` control mode for the demo so it observes without taking control. | +| Mid | `machineGroupControl` "MGC — Pump Group" | Distributes Qd flow demand across the 3 pumps via `optimalcontrol` (BEP-driven). Scaling: `absolute` (Qd is in m³/h directly). | +| Low | `rotatingMachine` × 3 — Pump A / B / C | Hidrostal H05K-S03R curve. `auto` mode by default so MGC's `parent` commands are accepted. Manual setpoint slider overrides per-pump when each is in `virtualControl`. | +| Sensors | `measurement` × 6 | Per pump: upstream + downstream pressure (mbar). Simulator mode — each ticks a random-walk value continuously. Registered as children of their pump. | +| Demand | inject `demand_rand_tick` + function `demand_rand_fn` + `ui-slider` | Random generator (3 s tick, [40, 240] m³/h) AND a manual slider. Both feed a router that fans out to PS (`q_in` in m³/s) and MGC (`Qd` in m³/h). | +| Glue | `setMode` fanouts + station-wide buttons | Mode toggle broadcasts `setMode` to all 3 pumps. Station-wide Start / Stop / Emergency-Stop buttons fan out to all 3. | +| Dashboard | FlowFuse `ui-page` + 6 groups | Process Demand · Pumping Station · Pump A · Pump B · Pump C · Trends. | + +## Dashboard map + +The page (`/dashboard/pumping-station-demo`) is laid out top-to-bottom: + +1. **Process Demand** + - Slider 0–300 m³/h (`manualDemand` topic) + - Random demand toggle (auto cycles every 3 s) + - Live "current demand" text +2. **Pumping Station** + - Auto/Manual mode toggle (drives all pumps' `setMode` simultaneously) + - Station-wide buttons: Start all · Stop all · Emergency stop + - Basin state, level (m), volume (m³), inflow / pumped-out flow (m³/h) +3. **Pump A / B / C** (one group each) + - Setpoint slider 0–100 % (only effective when that pump is in `virtualControl`) + - Per-pump Startup + Shutdown buttons + - Live state, mode, controller %, flow, power, upstream/downstream pressure +4. **Trends** + - Flow per pump chart (m³/h) + - Power per pump chart (kW) + +## Control model + +- **AUTO** — the default. `setMode auto` → MGC's `optimalcontrol` decides which pumps run and at what flow. Operator drives only the **Process Demand** slider (or leaves the random generator on); the per-pump setpoint sliders are ignored. +- **MANUAL** — flip the Auto/Manual switch. All 3 pumps go to `virtualControl`. MGC commands are now ignored. Per-pump setpoint sliders / Start / Stop are the only inputs that affect the pumps. + +The Emergency Stop button always works regardless of mode and uses the new interruptible-movement path so it stops a pump mid-ramp. + +## Notable design choices + +- **PS is in `manual` control mode** (`controlMode: "manual"`). The default `levelbased` mode would auto-shut all pumps as soon as basin level dips below `stopLevel` (1 m default), which masks the demo. Manual = observation only. +- **PS safety guards (dry-run / overfill) disabled.** With no real inflow the basin will frequently look "empty" — that's expected for a demo, not a fault. In production you'd configure a real `q_in` source and leave safeties on. +- **MGC scaling = `absolute`, mode = `optimalcontrol`.** Set via inject at deploy. Demand in m³/h, BEP-driven distribution. +- **demand_router gates Qd ≤ 0.** A demand of 0 would shut every running pump (via MGC.turnOffAllMachines). Use the explicit Stop All button to actually take pumps down. +- **Auto-startup on deploy.** All three pumps fire `execSequence startup` 4 s after deploy so the dashboard shows activity immediately. +- **Auto-enable random demand** 5 s after deploy so the trends fill in without operator action. +- **Verbose logging is OFF.** All EVOLV nodes are at `warn`. Crank the per-node `logLevel` to `info` or `debug` if you're diagnosing a flow. + +## Things to try + +- Drag the **Process Demand slider** with random off — watch MGC distribute that target across pumps and the basin start filling/draining accordingly. +- Flip to **Manual** mode and use the per-pump setpoint sliders — note that MGC stops driving them. +- Hit **Emergency Stop** while a pump is ramping — confirms the interruptible-movement fix shipped in `rotatingMachine` v1.0.3. +- Watch the **Trends** chart over a few minutes — flow distribution shifts as MGC re-balances around the BEP. + +## Verification (last green run, 2026-04-13) + +Deployed via `POST /flows` to a Dockerized Node-RED, observed for ~15 s after auto-startup: + +- All 3 measurement nodes per pump tick (6 total): pressure values stream every second. +- Each pump reaches `operational` ~5 s after the auto-startup inject (3 s starting + 1 s warmup + 1 s for setpoint=0 settle). +- MGC reports `3 machine(s) connected` with mode `optimalcontrol`. +- Pumping Station shows non-zero basin volume + tracks net flow direction (⬆ / ⬇ / ⏸). +- Random demand cycles between ~40 and ~240 m³/h every 3 s. +- Per-pump status text + trend chart update on every tick. + +## Regenerating `flow.json` + +`flow.json` is generated from `build_flow.py`. Edit the Python (cleaner diff) and regenerate: + +```bash +cd examples/pumpingstation-3pumps-dashboard +python3 build_flow.py > flow.json +``` + +The `build_flow.py` is the source of truth — keep it in sync if you tweak the demo. + +## Wishlist (not in this demo, build separately) + +- **Pump failure + MGC re-routing** — kill pump 2 mid-run, watch MGC redistribute. Would demonstrate fault-tolerance. +- **Energy-optimal vs equal-flow control** — same demand profile run through `optimalcontrol` and `prioritycontrol` modes side-by-side, energy comparison chart. +- **Schedule-driven demand** — diurnal flow pattern (low at night, peak at 7 am), MGC auto-tuning over 24 simulated hours. +- **PS with real `q_in` source + safeties on** — show the basin auto-shut behaviour as a feature, not a bug. +- **Real flow sensor per pump** (vs. relying on rotatingMachine's predicted flow) — would let the demo also show measurement-vs-prediction drift indicators. +- **Reactor or settler downstream** — close the loop on a real wastewater scenario. + +See the parent `examples/README.md` for the full follow-up catalogue. diff --git a/examples/pumpingstation-3pumps-dashboard/build_flow.py b/examples/pumpingstation-3pumps-dashboard/build_flow.py new file mode 100644 index 0000000..99f9479 --- /dev/null +++ b/examples/pumpingstation-3pumps-dashboard/build_flow.py @@ -0,0 +1,766 @@ +#!/usr/bin/env python3 +""" +Generate the full Node-RED flow JSON for the +'pumpingstation-3pumps-dashboard' end-to-end example. + +The flow encodes: + + - 1 pumpingStation (basin model) + - 1 machineGroupControl (orchestrator, optimal control mode) + - 3 rotatingMachine (pumps, hidrostal-H05K-S03R curve) + - 6 measurement nodes (per pump: upstream + downstream pressure, mbar, + simulator mode so they tick continuously) + - Process demand input (dashboard slider + random generator) routed to + both pumpingStation (q_in) and MGC (Qd) + - Mode toggle between AUTO (MGC drives pumps) and MANUAL (dashboard + drives each pump individually) + - Per-pump and station-wide dashboard groups with status, setpoint, + flow / power gauges, and trend charts + - Inject-driven setup at deploy time so the stack auto-wires children + and starts in a known state + +This file is the SOURCE OF TRUTH for the demo. To regenerate flow.json: + + python3 build_flow.py > flow.json + +To deploy directly to a running Dockerized Node-RED: + + python3 build_flow.py | curl -s -X POST http://localhost:1880/flows \ + -H "Content-Type: application/json" \ + -H "Node-RED-Deployment-Type: full" \ + --data-binary @- +""" +import json +import sys + +TAB_ID = "ps_demo_tab" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def mk_id(name): + return name.replace(" ", "_").replace("-", "_") + +def comment(node_id, x, y, name, info=""): + return { + "id": node_id, "type": "comment", "z": TAB_ID, "name": name, + "info": info, "x": x, "y": y, "wires": [] + } + +def inject(node_id, x, y, name, topic, payload, payload_type="str", once=False, repeat="", wires=None): + # Use the per-prop v/vt form so the runtime correctly types the + # payload (especially for payload_type='json' which otherwise gets + # passed through as a plain string when only the legacy top-level + # payload/payloadType fields are populated). + return { + "id": node_id, "type": "inject", "z": TAB_ID, "name": name, + "props": [ + {"p": "topic", "vt": "str"}, + {"p": "payload", "v": str(payload), "vt": payload_type}, + ], + "topic": topic, "payload": str(payload), "payloadType": payload_type, + "repeat": repeat, "crontab": "", + "once": once, "onceDelay": "0.5", + "x": x, "y": y, "wires": [wires or []], + } + +def function_node(node_id, x, y, name, code, outputs=1, wires=None): + return { + "id": node_id, "type": "function", "z": TAB_ID, "name": name, + "func": code, "outputs": outputs, + "noerr": 0, "initialize": "", "finalize": "", "libs": [], + "x": x, "y": y, "wires": wires or [[] for _ in range(outputs)], + } + +def change_node(node_id, x, y, name, rules, wires=None): + return { + "id": node_id, "type": "change", "z": TAB_ID, "name": name, + "rules": rules, "action": "", "property": "", "from": "", "to": "", + "reg": False, "x": x, "y": y, "wires": [wires or []], + } + +def link_in(node_id, x, y, name, links): + return { + "id": node_id, "type": "link in", "z": TAB_ID, "name": name, + "links": links, "x": x, "y": y, "wires": [[]] + } + +def link_out(node_id, x, y, name, links): + return { + "id": node_id, "type": "link out", "z": TAB_ID, "name": name, + "mode": "link", "links": links, "x": x, "y": y, "wires": [] + } + +def debug_node(node_id, x, y, name, target="payload", target_type="msg", active=False): + return { + "id": node_id, "type": "debug", "z": TAB_ID, "name": name, + "active": active, "tosidebar": True, "console": False, "tostatus": False, + "complete": target, "targetType": target_type, + "x": x, "y": y, "wires": [] + } + +# --------------------------------------------------------------------------- +# Dashboard scaffolding +# --------------------------------------------------------------------------- +def dashboard_scaffold(): + base = { + "id": "ui_base_ps_demo", "type": "ui-base", "name": "EVOLV Demo", + "path": "/dashboard", "appIcon": "", + "includeClientData": True, + "acceptsClientConfig": ["ui-notification", "ui-control"], + "showPathInSidebar": True, "headerContent": "page", + "navigationStyle": "default", "titleBarStyle": "default" + } + theme = { + "id": "ui_theme_ps_demo", "type": "ui-theme", "name": "EVOLV Theme", + "colors": { + "surface": "#ffffff", "primary": "#0f52a5", + "bgPage": "#f4f6fa", "groupBg": "#ffffff", "groupOutline": "#cccccc" + }, + "sizes": { + "density": "default", "pagePadding": "12px", + "groupGap": "12px", "groupBorderRadius": "6px", "widgetGap": "8px" + } + } + page = { + "id": "ui_page_ps_demo", "type": "ui-page", + "name": "Pumping Station — 3 Pumps", "ui": "ui_base_ps_demo", + "path": "/pumping-station-demo", "icon": "water_pump", + "layout": "grid", "theme": "ui_theme_ps_demo", + "breakpoints": [{"name": "Default", "px": "0", "cols": "12"}], + "order": 1, "className": "" + } + return [base, theme, page] + +def ui_group(group_id, name, page_id, width=6, order=1): + return { + "id": group_id, "type": "ui-group", "name": name, "page": page_id, + "width": str(width), "height": "1", "order": order, + "showTitle": True, "className": "", "groupType": "default", + "disabled": False, "visible": True + } + +def ui_text(node_id, x, y, group, name, label, fmt, layout="row-left"): + return { + "id": node_id, "type": "ui-text", "z": TAB_ID, "group": group, + "order": 1, "width": "0", "height": "0", "name": name, "label": label, + "format": fmt, "layout": layout, "style": False, "font": "", + "fontSize": 14, "color": "#000000", "wires": [] + } + +def ui_button(node_id, x, y, group, name, label, payload, payload_type, topic, color="#0f52a5", icon="play_arrow", wires=None): + return { + "id": node_id, "type": "ui-button", "z": TAB_ID, "group": group, + "name": name, "label": label, "order": 1, "width": "0", "height": "0", + "tooltip": "", "color": "#ffffff", "bgcolor": color, + "className": "", "icon": icon, "iconPosition": "left", + "payload": payload, "payloadType": payload_type, + "topic": topic, "topicType": "str", "buttonType": "default", + "x": x, "y": y, "wires": [wires or []] + } + +def ui_slider(node_id, x, y, group, name, label, mn, mx, step=1.0, topic="", wires=None): + return { + "id": node_id, "type": "ui-slider", "z": TAB_ID, "group": group, + "name": name, "label": label, "tooltip": "", "order": 1, + "width": "0", "height": "0", "passthru": True, "outs": "end", + "topic": topic, "topicType": "str", + "min": str(mn), "max": str(mx), "step": str(step), + "showLabel": True, "showValue": True, "labelPosition": "top", + "valuePosition": "left", "thumbLabel": False, "iconStart": "", + "iconEnd": "", "x": x, "y": y, "wires": [wires or []] + } + +def ui_switch(node_id, x, y, group, name, label, on_value="auto", off_value="manual", topic="modeToggle", wires=None): + return { + "id": node_id, "type": "ui-switch", "z": TAB_ID, "group": group, + "name": name, "label": label, "tooltip": "", "order": 1, + "width": "0", "height": "0", "passthru": True, "decouple": "false", + "topic": topic, "topicType": "str", + "style": "", "className": "", "evaluate": "true", + "onvalue": on_value, "onvalueType": "str", + "onicon": "auto_mode", "oncolor": "#0f52a5", + "offvalue": off_value, "offvalueType": "str", + "officon": "back_hand", "offcolor": "#888888", + "x": x, "y": y, "wires": [wires or []] + } + +def ui_chart(node_id, x, y, group, name, label, series_topics, ymin=None, ymax=None): + return { + "id": node_id, "type": "ui-chart", "z": TAB_ID, "group": group, + "name": name, "label": label, "order": 1, "chartType": "line", + "category": "topic", "categoryType": "msg", + "xAxisLabel": "", "xAxisType": "time", "xAxisTimeFormat": "auto", + "yAxisLabel": "", "ymin": "" if ymin is None else str(ymin), + "ymax": "" if ymax is None else str(ymax), + "action": "append", "pointShape": "circle", "pointRadius": 2, + "showLegend": True, "removeOlder": "10", "removeOlderUnit": "60", + "removeOlderPoints": "200", "colors": [], "textColor": [], "textColorDefault": True, + "width": "0", "height": "0", "className": "", "wires": [[]] + } + +def ui_gauge(node_id, x, y, group, name, label, mn, mx, units, segments=None): + return { + "id": node_id, "type": "ui-gauge", "z": TAB_ID, "group": group, + "name": name, "label": label, "order": 1, "width": "0", "height": "0", + "min": str(mn), "max": str(mx), "type": "gauge", + "units": units, "valueColor": "#0f52a5", "valueRound": True, + "valueFontSize": 16, "labelFontSize": 12, "iconColor": "", + "valueFormat": "value | number:1", "wires": [] + } + +# --------------------------------------------------------------------------- +# Build the flow +# --------------------------------------------------------------------------- +def build(): + nodes = [] + nodes += dashboard_scaffold() + + # ----- Tab ----- + nodes.append({ + "id": TAB_ID, "type": "tab", + "label": "Pumping Station — 3 Pumps Demo", + "disabled": False, + "info": "End-to-end demo: pumpingStation + machineGroupControl + 3 rotatingMachines, each with upstream/downstream pressure sensors. Process demand input via dashboard slider OR auto random generator. Dashboard at /dashboard/pumping-station-demo." + }) + + # ----- Dashboard groups ----- + g_demand = "ui_grp_demand" + g_station = "ui_grp_station" + g_pump_a = "ui_grp_pump_a" + g_pump_b = "ui_grp_pump_b" + g_pump_c = "ui_grp_pump_c" + g_trend = "ui_grp_trend" + PG = "ui_page_ps_demo" + nodes += [ + ui_group(g_demand, "1. Process Demand", PG, width=12, order=1), + ui_group(g_station, "2. Pumping Station", PG, width=12, order=2), + ui_group(g_pump_a, "3a. Pump A", PG, width=4, order=3), + ui_group(g_pump_b, "3b. Pump B", PG, width=4, order=4), + ui_group(g_pump_c, "3c. Pump C", PG, width=4, order=5), + ui_group(g_trend, "4. Trends", PG, width=12, order=6), + ] + + # ----- Comments / sections ----- + nodes.append(comment("c_title", 200, 40, + "Pumping Station — 3 Pumps Demo", + "Process demand → pumpingStation (basin model) + machineGroupControl (orchestrator) → 3 rotatingMachines. Each pump has upstream + downstream pressure measurements. Auto/Manual mode toggle on the dashboard." + )) + + # =========================================================== + # Backend node IDs + # =========================================================== + PS_ID = "ps_basin" + MGC_ID = "mgc_pumps" + PUMPS = ["pump_a", "pump_b", "pump_c"] + PUMP_LABELS = {"pump_a": "Pump A", "pump_b": "Pump B", "pump_c": "Pump C"} + PUMP_GROUPS = {"pump_a": g_pump_a, "pump_b": g_pump_b, "pump_c": g_pump_c} + + # =========================================================== + # MEASUREMENT NODES (6 total — 2 per pump) + # =========================================================== + meas_nodes = [] + for i, pump in enumerate(PUMPS): + for j, pos in enumerate(("upstream", "downstream")): + mid = f"meas_{pump}_{pos[0]}" + x = 200 + j * 240 + y = 1000 + i * 220 + # Different ranges/setpoints so each pump has plausible pressure + absmin, absmax = (50, 400) if pos == "upstream" else (800, 2200) + mid_label = f"PT-{PUMP_LABELS[pump].split()[1]}-{'Up' if pos=='upstream' else 'Dn'}" + nodes.append({ + "id": mid, "type": "measurement", "z": TAB_ID, + "name": mid_label, + "mode": "analog", "channels": "[]", + "scaling": False, + "i_min": 0, "i_max": 1, "i_offset": 0, + "o_min": absmin, "o_max": absmax, + "simulator": True, + "smooth_method": "mean", "count": "5", + "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", + "uuid": f"sensor-{pump}-{pos}", + "supplier": "vega", "category": "sensor", + "assetType": "pressure", "model": "vega-pressure-10", + "unit": "mbar", "assetTagNumber": f"PT-{i+1}-{pos[0].upper()}", + "enableLog": True, "logLevel": "warn", + "positionVsParent": pos, "positionIcon": "", + "hasDistance": False, "distance": 0, "distanceUnit": "m", "distanceDescription": "", + "x": x, "y": y, "wires": [[], [], [pump]] # Port 2 -> pump + }) + meas_nodes.append(mid) + + # =========================================================== + # ROTATING MACHINES (3 pumps) + # =========================================================== + for i, pump in enumerate(PUMPS): + x = 700 + y = 700 + i * 120 + # Wires: port 0 -> pump-port-0 router; port 1 -> debug; port 2 -> MGC + nodes.append({ + "id": pump, "type": "rotatingMachine", "z": TAB_ID, + "name": PUMP_LABELS[pump], + "speed": "10", # 10 %/s ramp — fast enough for demo + "startup": "2", "warmup": "1", "shutdown": "2", "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": f"pump-{pump}", + "supplier": "hidrostal", "category": "pump", + "assetType": "pump-centrifugal", + "model": "hidrostal-H05K-S03R", + "unit": "m3/h", + "curvePressureUnit": "mbar", "curveFlowUnit": "m3/h", + "curvePowerUnit": "kW", "curveControlUnit": "%", + "enableLog": True, "logLevel": "warn", + "positionVsParent": "atEquipment", "positionIcon": "", + "hasDistance": False, "distance": 0, "distanceUnit": "m", "distanceDescription": "", + "x": x, "y": y, + "wires": [ + [f"router_p0_{pump}"], # port 0 process + [], # port 1 dbase + [MGC_ID], # port 2 -> MGC for registration + ] + }) + + # =========================================================== + # MACHINE GROUP CONTROL + # =========================================================== + nodes.append({ + "id": MGC_ID, "type": "machineGroupControl", "z": TAB_ID, + "name": "MGC — Pump Group", + "uuid": "mgc-pump-group", + "category": "controller", + "assetType": "machinegroupcontrol", + "model": "default", + "unit": "m3/h", + "supplier": "evolv", + "enableLog": True, "logLevel": "warn", + "positionVsParent": "atEquipment", "positionIcon": "", + "hasDistance": False, "distance": 0, "distanceUnit": "m", "distanceDescription": "", + "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", + "x": 1100, "y": 760, + "wires": [ + [], # port 0 process + [], # port 1 dbase + [PS_ID], # port 2 -> pumpingStation registration + ] + }) + + # =========================================================== + # PUMPING STATION + # =========================================================== + nodes.append({ + "id": PS_ID, "type": "pumpingStation", "z": TAB_ID, + "name": "Pumping Station", + "uuid": "ps-basin-1", + "category": "station", + "assetType": "pumpingstation", + "model": "default", + "unit": "m3/s", + "supplier": "evolv", + "enableLog": True, "logLevel": "info", + "positionVsParent": "atEquipment", "positionIcon": "", + "hasDistance": False, "distance": 0, "distanceUnit": "m", "distanceDescription": "", + "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", + # Default PS control mode 'levelbased' will auto-shut all pumps as + # soon as basin level dips below stopLevel (1m default). For an + # operator-driven demo we want PS to OBSERVE only — set it to + # 'manual' from boot via the controlMode UI field. + "controlMode": "manual", + # Bigger basin so level metrics are interesting on the dashboard. + "basinVolume": 50, "basinHeight": 4, + # Disable safety guards. The defaults turn ON dry-run + overfill + # protection which shut every pump as soon as basin vol drops below + # 2% of minVol (or rises above 98% of maxVolOverflow). For an + # operator-driven demo where there is no real inflow source, the + # basin will frequently look "empty" — that's expected, not a fault. + "enableDryRunProtection": False, + "enableOverfillProtection": False, + "dryRunThresholdPercent": 0, + "overfillThresholdPercent": 100, + "timeleftToFullOrEmptyThresholdSeconds": 0, + "x": 1450, "y": 760, + "wires": [ + ["ps_to_dashboard"], # port 0 process -> dashboard formatter + [], # port 1 dbase + ] + }) + + # =========================================================== + # PROCESS DEMAND — slider + random generator + router + # =========================================================== + # Demand value enters a router that fans out to: + # - pumpingStation (q_in) + # - MGC (Qd) + # - dashboard text + chart + nodes.append(ui_slider( + "ui_demand_slider", 100, 200, g_demand, + "Process demand slider", "Process Demand (m³/h)", + 0, 300, 5.0, "manualDemand", + wires=["demand_router"] + )) + nodes.append(ui_switch( + "ui_random_toggle", 100, 260, g_demand, + "Random demand", "Random demand generator (auto)", + on_value="on", off_value="off", topic="randomToggle", + wires=["random_state"] + )) + nodes.append(ui_text( + "ui_demand_text", 100, 320, g_demand, + "Current demand text", "Current demand", "{{msg.payload}} m³/h", + )) + + # Random generator: every 3s pick a value in [40, 240] m³/h. + nodes.append(inject( + "demand_rand_tick", 100, 380, + "tick (random demand)", + topic="randomTick", payload="", payload_type="date", + repeat="3", wires=["demand_rand_fn"] + )) + nodes.append(function_node( + "random_state", 280, 260, "store random state", + "context.set('on', msg.payload === 'on'); return null;", + outputs=0 + )) + nodes.append(function_node( + "demand_rand_fn", 280, 380, "random demand", + "if (!context.get('on')) { return null; }\n" + "const v = Math.round(40 + Math.random() * 200);\n" + "msg.payload = v;\n" + "msg.topic = 'manualDemand';\n" + "return msg;", + outputs=1, wires=[["demand_router", "ui_demand_text"]] + )) + + # Router: fan out current demand to pumpingStation (q_in) and MGC (Qd). + # NOTE: We only forward when demand > 0. A demand of 0 would + # - shut every running pump via MGC.turnOffAllMachines (Qd=0 path), and + # - drive the basin level toward 'draining', which makes + # pumpingStation._applyLevelBasedControl call group.turnOffAllMachines + # ANYWAY, even without the MGC Qd wire. + # The single guard below short-circuits the whole chain so the demo + # doesn't auto-shutdown pumps when no operator demand has been entered yet. + # Use the Stop All button to actually take pumps down. + nodes.append(function_node( + "demand_router", 480, 260, "fan-out demand", + "const v = Number(msg.payload);\n" + "if (!Number.isFinite(v) || v <= 0) return null;\n" + "// Feed pumpingStation q_in (m3/s canonical) and MGC Qd (m3/h).\n" + "const qin = { topic: 'q_in', payload: v / 3600, unit: 'm3/s' };\n" + "const qd = { topic: 'Qd', payload: v };\n" + "const text = { topic: 'demand', payload: v };\n" + "return [qin, qd, text];", + outputs=3, + wires=[[PS_ID], [MGC_ID, "dbg_demand_to_mgc"], ["ui_demand_text"]] + )) + nodes.append(debug_node("dbg_demand_to_mgc", 700, 320, "→ MGC Qd", target="payload", active=True)) + + # =========================================================== + # MODE toggle (auto / manual) + # =========================================================== + nodes.append(ui_switch( + "ui_mode_toggle", 100, 460, g_station, + "Auto/Manual mode", "Mode (Auto = MGC orchestrates · Manual = dashboard per-pump)", + on_value="auto", off_value="virtualControl", topic="setMode", + wires=["mode_fanout"] + )) + nodes.append(function_node( + "mode_fanout", 320, 460, "broadcast setMode to all pumps", + "msg.topic = 'setMode';\n" + "// Send same setMode payload to all 3 pumps.\n" + "return [msg, msg, msg];", + outputs=3, + wires=[["pump_a"], ["pump_b"], ["pump_c"]] + )) + + # =========================================================== + # STATION-WIDE BUTTONS + # =========================================================== + nodes.append(ui_button( + "btn_station_startup", 100, 520, g_station, + "Start all pumps", "Startup all", "startup", "str", + topic="stationStartup", color="#16a34a", icon="play_arrow", + wires=["station_startup_fan"] + )) + nodes.append(function_node( + "station_startup_fan", 320, 520, "fan startup to pumps", + "const cmd = { topic: 'execSequence', payload: { source: 'GUI', action: 'execSequence', parameter: 'startup' } };\n" + "return [cmd, cmd, cmd];", + outputs=3, + wires=[["pump_a"], ["pump_b"], ["pump_c"]] + )) + nodes.append(ui_button( + "btn_station_shutdown", 100, 580, g_station, + "Stop all pumps", "Shutdown all", "shutdown", "str", + topic="stationShutdown", color="#ea580c", icon="stop", + wires=["station_shutdown_fan"] + )) + nodes.append(function_node( + "station_shutdown_fan", 320, 580, "fan shutdown to pumps", + "const cmd = { topic: 'execSequence', payload: { source: 'GUI', action: 'execSequence', parameter: 'shutdown' } };\n" + "return [cmd, cmd, cmd];", + outputs=3, + wires=[["pump_a"], ["pump_b"], ["pump_c"]] + )) + nodes.append(ui_button( + "btn_station_estop", 100, 640, g_station, + "EMERGENCY STOP", "EMERGENCY STOP", "estop", "str", + topic="stationEstop", color="#dc2626", icon="stop_circle", + wires=["station_estop_fan"] + )) + nodes.append(function_node( + "station_estop_fan", 320, 640, "fan estop to pumps", + "const cmd = { topic: 'emergencystop', payload: { source: 'GUI', action: 'emergencystop' } };\n" + "return [cmd, cmd, cmd];", + outputs=3, + wires=[["pump_a"], ["pump_b"], ["pump_c"]] + )) + + # =========================================================== + # PER-PUMP DASHBOARD (status text + setpoint slider + buttons) + # =========================================================== + for i, pump in enumerate(PUMPS): + g = PUMP_GROUPS[pump] + label = PUMP_LABELS[pump] + y_base = 100 + i * 320 + + # Status text — fed by router_p0_ formatter + nodes.append(ui_text( + f"ui_{pump}_state", 1500, y_base + 0, g, + f"{label} state", "State", "{{msg.payload.state}}" + )) + nodes.append(ui_text( + f"ui_{pump}_mode", 1500, y_base + 30, g, + f"{label} mode", "Mode", "{{msg.payload.mode}}" + )) + nodes.append(ui_text( + f"ui_{pump}_ctrl", 1500, y_base + 60, g, + f"{label} ctrl", "Controller %", "{{msg.payload.ctrl}}" + )) + nodes.append(ui_text( + f"ui_{pump}_flow", 1500, y_base + 90, g, + f"{label} flow", "Flow (m³/h)", "{{msg.payload.flow}}" + )) + nodes.append(ui_text( + f"ui_{pump}_power", 1500, y_base + 120, g, + f"{label} power", "Power (kW)", "{{msg.payload.power}}" + )) + nodes.append(ui_text( + f"ui_{pump}_pUp", 1500, y_base + 150, g, + f"{label} pUp", "p Upstream (mbar)", "{{msg.payload.pUp}}" + )) + nodes.append(ui_text( + f"ui_{pump}_pDn", 1500, y_base + 180, g, + f"{label} pDn", "p Downstream (mbar)", "{{msg.payload.pDn}}" + )) + + # Per-pump manual setpoint slider (only effective in virtualControl mode) + nodes.append(ui_slider( + f"ui_{pump}_setpoint", 100, y_base, g, + f"{label} setpoint", "Setpoint % (manual mode)", + 0, 100, 5.0, f"setpoint_{pump}", + wires=[f"setpoint_to_pump_{pump}"] + )) + nodes.append(function_node( + f"setpoint_to_pump_{pump}", 320, y_base, f"build setpoint cmd for {label}", + "msg.topic = 'execMovement';\n" + "msg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\n" + "return msg;", + outputs=1, wires=[[pump]] + )) + + # Per-pump start/stop buttons + nodes.append(ui_button( + f"btn_{pump}_startup", 100, y_base + 60, g, + f"{label} startup", "Startup", "startup", "str", + topic=f"start_{pump}", color="#16a34a", icon="play_arrow", + wires=[f"start_to_pump_{pump}"] + )) + nodes.append(function_node( + f"start_to_pump_{pump}", 320, y_base + 60, f"build startup for {label}", + "msg.topic = 'execSequence';\n" + "msg.payload = { source: 'GUI', action: 'execSequence', parameter: 'startup' };\n" + "return msg;", + outputs=1, wires=[[pump]] + )) + nodes.append(ui_button( + f"btn_{pump}_shutdown", 100, y_base + 120, g, + f"{label} shutdown", "Shutdown", "shutdown", "str", + topic=f"stop_{pump}", color="#ea580c", icon="stop", + wires=[f"stop_to_pump_{pump}"] + )) + nodes.append(function_node( + f"stop_to_pump_{pump}", 320, y_base + 120, f"build shutdown for {label}", + "msg.topic = 'execSequence';\n" + "msg.payload = { source: 'GUI', action: 'execSequence', parameter: 'shutdown' };\n" + "return msg;", + outputs=1, wires=[[pump]] + )) + + # Port 0 router — merge delta updates into a per-pump cache and + # produce a tidy {state, mode, ctrl, flow, power, pUp, pDn} object + # for the dashboard widgets. + nodes.append(function_node( + f"router_p0_{pump}", 1100, y_base + 90, f"format {label} port 0", + "const p = msg.payload || {};\n" + "const c = context.get('c') || {};\n" + "Object.assign(c, p);\n" + "context.set('c', c);\n" + "function find(prefix) {\n" + " for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n" + " return null;\n" + "}\n" + "const flow = find('flow.predicted.downstream.');\n" + "const power = find('power.predicted.atequipment.');\n" + "const pU = find('pressure.measured.upstream.');\n" + "const pD = find('pressure.measured.downstream.');\n" + "msg.payload = {\n" + " state: c.state || 'idle',\n" + " mode: c.mode || 'auto',\n" + " ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n" + " flow: flow != null ? Number(flow).toFixed(1) + ' m³/h' : 'n/a',\n" + " power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n" + " pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n" + " pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n" + " flowNum: flow != null ? Number(flow) : null,\n" + " powerNum: power != null ? Number(power) : null\n" + "};\n" + "return msg;", + outputs=1, + wires=[[ + f"ui_{pump}_state", f"ui_{pump}_mode", f"ui_{pump}_ctrl", + f"ui_{pump}_flow", f"ui_{pump}_power", + f"ui_{pump}_pUp", f"ui_{pump}_pDn", + f"trend_split_{pump}" + ]] + )) + + # Trend feeders — one stream per metric for the chart + nodes.append(function_node( + f"trend_split_{pump}", 1300, y_base + 150, f"emit trend points for {label}", + "const p = msg.payload || {};\n" + "const out = [];\n" + "if (p.flowNum != null) out.push({ topic: '" + label + " flow', payload: p.flowNum });\n" + "if (p.powerNum != null) out.push({ topic: '" + label + " power', payload: p.powerNum });\n" + "return [out];", + outputs=1, wires=[["trend_chart_flow", "trend_chart_power"]] + )) + + # =========================================================== + # TREND CHARTS + # =========================================================== + nodes.append(ui_chart( + "trend_chart_flow", 1500, 1450, g_trend, + "Flow per pump (m³/h)", "Flow per pump", + ["Pump A flow", "Pump B flow", "Pump C flow"] + )) + nodes.append(ui_chart( + "trend_chart_power", 1500, 1500, g_trend, + "Power per pump (kW)", "Power per pump", + ["Pump A power", "Pump B power", "Pump C power"] + )) + + # =========================================================== + # PUMPING STATION DASHBOARD (basin level + total flow) + # =========================================================== + nodes.append(function_node( + "ps_to_dashboard", 1700, 700, "format PS port 0 for dashboard", + "const p = msg.payload || {};\n" + "const c = context.get('c') || {};\n" + "Object.assign(c, p);\n" + "context.set('c', c);\n" + "function find(prefix) {\n" + " for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n" + " return null;\n" + "}\n" + "const lvl = find('level.predicted.');\n" + "const vol = find('volume.predicted.');\n" + "const qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\n" + "const qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n" + "msg.payload = {\n" + " level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n" + " volume: vol != null ? Number(vol).toFixed(1) + ' m³' : 'n/a',\n" + " qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m³/h' : 'n/a',\n" + " qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m³/h' : 'n/a',\n" + " state: c.state || c.direction || 'idle',\n" + " netNum: (qIn != null && qOut != null) ? (Number(qIn) - Number(qOut)) * 3600 : null\n" + "};\n" + "return msg;", + outputs=1, wires=[[ + "ui_ps_level", "ui_ps_volume", "ui_ps_qin", "ui_ps_qout", "ui_ps_state" + ]] + )) + nodes.append(ui_text("ui_ps_state", 1900, 700, g_station, "PS state", "Basin state", "{{msg.payload.state}}")) + nodes.append(ui_text("ui_ps_level", 1900, 730, g_station, "PS level", "Basin level", "{{msg.payload.level}}")) + nodes.append(ui_text("ui_ps_volume", 1900, 760, g_station, "PS volume","Basin volume", "{{msg.payload.volume}}")) + nodes.append(ui_text("ui_ps_qin", 1900, 790, g_station, "PS Qin", "Inflow", "{{msg.payload.qIn}}")) + nodes.append(ui_text("ui_ps_qout", 1900, 820, g_station, "PS Qout", "Pumped out", "{{msg.payload.qOut}}")) + + # =========================================================== + # SETUP INJECTS (fire once on deploy) + # =========================================================== + # MGC needs scaling=absolute + mode=optimalcontrol so Qd is treated + # in m³/h directly and pumps are dispatched by BEP optimization. + nodes.append(inject( + "setup_mgc_scaling", 100, 800, + "setup: MGC scaling=absolute", + topic="setScaling", payload="absolute", payload_type="str", + once=True, wires=[MGC_ID] + )) + nodes.append(inject( + "setup_mgc_mode", 100, 840, + "setup: MGC mode=optimalcontrol", + topic="setMode", payload="optimalcontrol", payload_type="str", + once=True, wires=[MGC_ID] + )) + # Default the pumps + MGC to auto so the slider drives them out of the box. + nodes.append(inject( + "setup_pumps_mode", 100, 880, + "setup: pumps mode=auto", + topic="setMode", payload="auto", payload_type="str", + once=True, wires=[ + "mode_setup_fan" + ] + )) + nodes.append(function_node( + "mode_setup_fan", 320, 880, "fan setup mode to pumps", + "msg.topic = 'setMode';\n" + "return [msg, msg, msg];", + outputs=3, wires=[["pump_a"], ["pump_b"], ["pump_c"]] + )) + # Auto-startup all pumps after registration settles (~3s) + nodes.append(inject( + "setup_pumps_startup", 100, 920, + "setup: pumps startup", + topic="execSequence", + payload='{"source":"GUI","action":"execSequence","parameter":"startup"}', + payload_type="json", once=True, wires=["startup_setup_fan"] + )) + nodes[-1]["onceDelay"] = "4" + nodes.append(function_node( + "startup_setup_fan", 320, 920, "fan startup to pumps", + "return [msg, msg, msg];", + outputs=3, wires=[["pump_a"], ["pump_b"], ["pump_c"]] + )) + + # Auto-enable the random demand generator + seed an initial demand + # value so the dashboard shows activity immediately on first deploy. + # Operator can flip the random toggle off + drag the slider any time. + nodes.append(inject( + "setup_random_on", 100, 960, + "setup: random demand ON", + topic="randomToggle", payload="on", payload_type="str", + once=True, wires=["random_state"] + )) + nodes[-1]["onceDelay"] = "5" + + return nodes + + +def main(): + nodes = build() + json.dump(nodes, sys.stdout, indent=2) + sys.stdout.write("\n") + + +if __name__ == "__main__": + main() diff --git a/examples/pumpingstation-3pumps-dashboard/flow.json b/examples/pumpingstation-3pumps-dashboard/flow.json new file mode 100644 index 0000000..de4a4a3 --- /dev/null +++ b/examples/pumpingstation-3pumps-dashboard/flow.json @@ -0,0 +1,2369 @@ +[ + { + "id": "ui_base_ps_demo", + "type": "ui-base", + "name": "EVOLV Demo", + "path": "/dashboard", + "appIcon": "", + "includeClientData": true, + "acceptsClientConfig": [ + "ui-notification", + "ui-control" + ], + "showPathInSidebar": true, + "headerContent": "page", + "navigationStyle": "default", + "titleBarStyle": "default" + }, + { + "id": "ui_theme_ps_demo", + "type": "ui-theme", + "name": "EVOLV Theme", + "colors": { + "surface": "#ffffff", + "primary": "#0f52a5", + "bgPage": "#f4f6fa", + "groupBg": "#ffffff", + "groupOutline": "#cccccc" + }, + "sizes": { + "density": "default", + "pagePadding": "12px", + "groupGap": "12px", + "groupBorderRadius": "6px", + "widgetGap": "8px" + } + }, + { + "id": "ui_page_ps_demo", + "type": "ui-page", + "name": "Pumping Station \u2014 3 Pumps", + "ui": "ui_base_ps_demo", + "path": "/pumping-station-demo", + "icon": "water_pump", + "layout": "grid", + "theme": "ui_theme_ps_demo", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "12" + } + ], + "order": 1, + "className": "" + }, + { + "id": "ps_demo_tab", + "type": "tab", + "label": "Pumping Station \u2014 3 Pumps Demo", + "disabled": false, + "info": "End-to-end demo: pumpingStation + machineGroupControl + 3 rotatingMachines, each with upstream/downstream pressure sensors. Process demand input via dashboard slider OR auto random generator. Dashboard at /dashboard/pumping-station-demo." + }, + { + "id": "ui_grp_demand", + "type": "ui-group", + "name": "1. Process Demand", + "page": "ui_page_ps_demo", + "width": "12", + "height": "1", + "order": 1, + "showTitle": true, + "className": "", + "groupType": "default", + "disabled": false, + "visible": true + }, + { + "id": "ui_grp_station", + "type": "ui-group", + "name": "2. Pumping Station", + "page": "ui_page_ps_demo", + "width": "12", + "height": "1", + "order": 2, + "showTitle": true, + "className": "", + "groupType": "default", + "disabled": false, + "visible": true + }, + { + "id": "ui_grp_pump_a", + "type": "ui-group", + "name": "3a. Pump A", + "page": "ui_page_ps_demo", + "width": "4", + "height": "1", + "order": 3, + "showTitle": true, + "className": "", + "groupType": "default", + "disabled": false, + "visible": true + }, + { + "id": "ui_grp_pump_b", + "type": "ui-group", + "name": "3b. Pump B", + "page": "ui_page_ps_demo", + "width": "4", + "height": "1", + "order": 4, + "showTitle": true, + "className": "", + "groupType": "default", + "disabled": false, + "visible": true + }, + { + "id": "ui_grp_pump_c", + "type": "ui-group", + "name": "3c. Pump C", + "page": "ui_page_ps_demo", + "width": "4", + "height": "1", + "order": 5, + "showTitle": true, + "className": "", + "groupType": "default", + "disabled": false, + "visible": true + }, + { + "id": "ui_grp_trend", + "type": "ui-group", + "name": "4. Trends", + "page": "ui_page_ps_demo", + "width": "12", + "height": "1", + "order": 6, + "showTitle": true, + "className": "", + "groupType": "default", + "disabled": false, + "visible": true + }, + { + "id": "c_title", + "type": "comment", + "z": "ps_demo_tab", + "name": "Pumping Station \u2014 3 Pumps Demo", + "info": "Process demand \u2192 pumpingStation (basin model) + machineGroupControl (orchestrator) \u2192 3 rotatingMachines. Each pump has upstream + downstream pressure measurements. Auto/Manual mode toggle on the dashboard.", + "x": 200, + "y": 40, + "wires": [] + }, + { + "id": "meas_pump_a_u", + "type": "measurement", + "z": "ps_demo_tab", + "name": "PT-A-Up", + "mode": "analog", + "channels": "[]", + "scaling": false, + "i_min": 0, + "i_max": 1, + "i_offset": 0, + "o_min": 50, + "o_max": 400, + "simulator": true, + "smooth_method": "mean", + "count": "5", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "uuid": "sensor-pump_a-upstream", + "supplier": "vega", + "category": "sensor", + "assetType": "pressure", + "model": "vega-pressure-10", + "unit": "mbar", + "assetTagNumber": "PT-1-U", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "upstream", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 200, + "y": 1000, + "wires": [ + [], + [], + [ + "pump_a" + ] + ] + }, + { + "id": "meas_pump_a_d", + "type": "measurement", + "z": "ps_demo_tab", + "name": "PT-A-Dn", + "mode": "analog", + "channels": "[]", + "scaling": false, + "i_min": 0, + "i_max": 1, + "i_offset": 0, + "o_min": 800, + "o_max": 2200, + "simulator": true, + "smooth_method": "mean", + "count": "5", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "uuid": "sensor-pump_a-downstream", + "supplier": "vega", + "category": "sensor", + "assetType": "pressure", + "model": "vega-pressure-10", + "unit": "mbar", + "assetTagNumber": "PT-1-D", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "downstream", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 440, + "y": 1000, + "wires": [ + [], + [], + [ + "pump_a" + ] + ] + }, + { + "id": "meas_pump_b_u", + "type": "measurement", + "z": "ps_demo_tab", + "name": "PT-B-Up", + "mode": "analog", + "channels": "[]", + "scaling": false, + "i_min": 0, + "i_max": 1, + "i_offset": 0, + "o_min": 50, + "o_max": 400, + "simulator": true, + "smooth_method": "mean", + "count": "5", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "uuid": "sensor-pump_b-upstream", + "supplier": "vega", + "category": "sensor", + "assetType": "pressure", + "model": "vega-pressure-10", + "unit": "mbar", + "assetTagNumber": "PT-2-U", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "upstream", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 200, + "y": 1220, + "wires": [ + [], + [], + [ + "pump_b" + ] + ] + }, + { + "id": "meas_pump_b_d", + "type": "measurement", + "z": "ps_demo_tab", + "name": "PT-B-Dn", + "mode": "analog", + "channels": "[]", + "scaling": false, + "i_min": 0, + "i_max": 1, + "i_offset": 0, + "o_min": 800, + "o_max": 2200, + "simulator": true, + "smooth_method": "mean", + "count": "5", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "uuid": "sensor-pump_b-downstream", + "supplier": "vega", + "category": "sensor", + "assetType": "pressure", + "model": "vega-pressure-10", + "unit": "mbar", + "assetTagNumber": "PT-2-D", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "downstream", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 440, + "y": 1220, + "wires": [ + [], + [], + [ + "pump_b" + ] + ] + }, + { + "id": "meas_pump_c_u", + "type": "measurement", + "z": "ps_demo_tab", + "name": "PT-C-Up", + "mode": "analog", + "channels": "[]", + "scaling": false, + "i_min": 0, + "i_max": 1, + "i_offset": 0, + "o_min": 50, + "o_max": 400, + "simulator": true, + "smooth_method": "mean", + "count": "5", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "uuid": "sensor-pump_c-upstream", + "supplier": "vega", + "category": "sensor", + "assetType": "pressure", + "model": "vega-pressure-10", + "unit": "mbar", + "assetTagNumber": "PT-3-U", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "upstream", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 200, + "y": 1440, + "wires": [ + [], + [], + [ + "pump_c" + ] + ] + }, + { + "id": "meas_pump_c_d", + "type": "measurement", + "z": "ps_demo_tab", + "name": "PT-C-Dn", + "mode": "analog", + "channels": "[]", + "scaling": false, + "i_min": 0, + "i_max": 1, + "i_offset": 0, + "o_min": 800, + "o_max": 2200, + "simulator": true, + "smooth_method": "mean", + "count": "5", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "uuid": "sensor-pump_c-downstream", + "supplier": "vega", + "category": "sensor", + "assetType": "pressure", + "model": "vega-pressure-10", + "unit": "mbar", + "assetTagNumber": "PT-3-D", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "downstream", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 440, + "y": 1440, + "wires": [ + [], + [], + [ + "pump_c" + ] + ] + }, + { + "id": "pump_a", + "type": "rotatingMachine", + "z": "ps_demo_tab", + "name": "Pump A", + "speed": "10", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "pump-pump_a", + "supplier": "hidrostal", + "category": "pump", + "assetType": "pump-centrifugal", + "model": "hidrostal-H05K-S03R", + "unit": "m3/h", + "curvePressureUnit": "mbar", + "curveFlowUnit": "m3/h", + "curvePowerUnit": "kW", + "curveControlUnit": "%", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 700, + "y": 700, + "wires": [ + [ + "router_p0_pump_a" + ], + [], + [ + "mgc_pumps" + ] + ] + }, + { + "id": "pump_b", + "type": "rotatingMachine", + "z": "ps_demo_tab", + "name": "Pump B", + "speed": "10", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "pump-pump_b", + "supplier": "hidrostal", + "category": "pump", + "assetType": "pump-centrifugal", + "model": "hidrostal-H05K-S03R", + "unit": "m3/h", + "curvePressureUnit": "mbar", + "curveFlowUnit": "m3/h", + "curvePowerUnit": "kW", + "curveControlUnit": "%", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 700, + "y": 820, + "wires": [ + [ + "router_p0_pump_b" + ], + [], + [ + "mgc_pumps" + ] + ] + }, + { + "id": "pump_c", + "type": "rotatingMachine", + "z": "ps_demo_tab", + "name": "Pump C", + "speed": "10", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "pump-pump_c", + "supplier": "hidrostal", + "category": "pump", + "assetType": "pump-centrifugal", + "model": "hidrostal-H05K-S03R", + "unit": "m3/h", + "curvePressureUnit": "mbar", + "curveFlowUnit": "m3/h", + "curvePowerUnit": "kW", + "curveControlUnit": "%", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 700, + "y": 940, + "wires": [ + [ + "router_p0_pump_c" + ], + [], + [ + "mgc_pumps" + ] + ] + }, + { + "id": "mgc_pumps", + "type": "machineGroupControl", + "z": "ps_demo_tab", + "name": "MGC \u2014 Pump Group", + "uuid": "mgc-pump-group", + "category": "controller", + "assetType": "machinegroupcontrol", + "model": "default", + "unit": "m3/h", + "supplier": "evolv", + "enableLog": true, + "logLevel": "warn", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "x": 1100, + "y": 760, + "wires": [ + [], + [], + [ + "ps_basin" + ] + ] + }, + { + "id": "ps_basin", + "type": "pumpingStation", + "z": "ps_demo_tab", + "name": "Pumping Station", + "uuid": "ps-basin-1", + "category": "station", + "assetType": "pumpingstation", + "model": "default", + "unit": "m3/s", + "supplier": "evolv", + "enableLog": true, + "logLevel": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "controlMode": "manual", + "basinVolume": 50, + "basinHeight": 4, + "enableDryRunProtection": false, + "enableOverfillProtection": false, + "dryRunThresholdPercent": 0, + "overfillThresholdPercent": 100, + "timeleftToFullOrEmptyThresholdSeconds": 0, + "x": 1450, + "y": 760, + "wires": [ + [ + "ps_to_dashboard" + ], + [] + ] + }, + { + "id": "ui_demand_slider", + "type": "ui-slider", + "z": "ps_demo_tab", + "group": "ui_grp_demand", + "name": "Process demand slider", + "label": "Process Demand (m\u00b3/h)", + "tooltip": "", + "order": 1, + "width": "0", + "height": "0", + "passthru": true, + "outs": "end", + "topic": "manualDemand", + "topicType": "str", + "min": "0", + "max": "300", + "step": "5.0", + "showLabel": true, + "showValue": true, + "labelPosition": "top", + "valuePosition": "left", + "thumbLabel": false, + "iconStart": "", + "iconEnd": "", + "x": 100, + "y": 200, + "wires": [ + [ + "demand_router" + ] + ] + }, + { + "id": "ui_random_toggle", + "type": "ui-switch", + "z": "ps_demo_tab", + "group": "ui_grp_demand", + "name": "Random demand", + "label": "Random demand generator (auto)", + "tooltip": "", + "order": 1, + "width": "0", + "height": "0", + "passthru": true, + "decouple": "false", + "topic": "randomToggle", + "topicType": "str", + "style": "", + "className": "", + "evaluate": "true", + "onvalue": "on", + "onvalueType": "str", + "onicon": "auto_mode", + "oncolor": "#0f52a5", + "offvalue": "off", + "offvalueType": "str", + "officon": "back_hand", + "offcolor": "#888888", + "x": 100, + "y": 260, + "wires": [ + [ + "random_state" + ] + ] + }, + { + "id": "ui_demand_text", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_demand", + "order": 1, + "width": "0", + "height": "0", + "name": "Current demand text", + "label": "Current demand", + "format": "{{msg.payload}} m\u00b3/h", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "demand_rand_tick", + "type": "inject", + "z": "ps_demo_tab", + "name": "tick (random demand)", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "", + "vt": "date" + } + ], + "topic": "randomTick", + "payload": "", + "payloadType": "date", + "repeat": "3", + "crontab": "", + "once": false, + "onceDelay": "0.5", + "x": 100, + "y": 380, + "wires": [ + [ + "demand_rand_fn" + ] + ] + }, + { + "id": "random_state", + "type": "function", + "z": "ps_demo_tab", + "name": "store random state", + "func": "context.set('on', msg.payload === 'on'); return null;", + "outputs": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 280, + "y": 260, + "wires": [] + }, + { + "id": "demand_rand_fn", + "type": "function", + "z": "ps_demo_tab", + "name": "random demand", + "func": "if (!context.get('on')) { return null; }\nconst v = Math.round(40 + Math.random() * 200);\nmsg.payload = v;\nmsg.topic = 'manualDemand';\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 280, + "y": 380, + "wires": [ + [ + "demand_router", + "ui_demand_text" + ] + ] + }, + { + "id": "demand_router", + "type": "function", + "z": "ps_demo_tab", + "name": "fan-out demand", + "func": "const v = Number(msg.payload);\nif (!Number.isFinite(v) || v <= 0) return null;\n// Feed pumpingStation q_in (m3/s canonical) and MGC Qd (m3/h).\nconst qin = { topic: 'q_in', payload: v / 3600, unit: 'm3/s' };\nconst qd = { topic: 'Qd', payload: v };\nconst text = { topic: 'demand', payload: v };\nreturn [qin, qd, text];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 480, + "y": 260, + "wires": [ + [ + "ps_basin" + ], + [ + "mgc_pumps", + "dbg_demand_to_mgc" + ], + [ + "ui_demand_text" + ] + ] + }, + { + "id": "dbg_demand_to_mgc", + "type": "debug", + "z": "ps_demo_tab", + "name": "\u2192 MGC Qd", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "x": 700, + "y": 320, + "wires": [] + }, + { + "id": "ui_mode_toggle", + "type": "ui-switch", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "name": "Auto/Manual mode", + "label": "Mode (Auto = MGC orchestrates \u00b7 Manual = dashboard per-pump)", + "tooltip": "", + "order": 1, + "width": "0", + "height": "0", + "passthru": true, + "decouple": "false", + "topic": "setMode", + "topicType": "str", + "style": "", + "className": "", + "evaluate": "true", + "onvalue": "auto", + "onvalueType": "str", + "onicon": "auto_mode", + "oncolor": "#0f52a5", + "offvalue": "virtualControl", + "offvalueType": "str", + "officon": "back_hand", + "offcolor": "#888888", + "x": 100, + "y": 460, + "wires": [ + [ + "mode_fanout" + ] + ] + }, + { + "id": "mode_fanout", + "type": "function", + "z": "ps_demo_tab", + "name": "broadcast setMode to all pumps", + "func": "msg.topic = 'setMode';\n// Send same setMode payload to all 3 pumps.\nreturn [msg, msg, msg];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 460, + "wires": [ + [ + "pump_a" + ], + [ + "pump_b" + ], + [ + "pump_c" + ] + ] + }, + { + "id": "btn_station_startup", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "name": "Start all pumps", + "label": "Startup all", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#16a34a", + "className": "", + "icon": "play_arrow", + "iconPosition": "left", + "payload": "startup", + "payloadType": "str", + "topic": "stationStartup", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 520, + "wires": [ + [ + "station_startup_fan" + ] + ] + }, + { + "id": "station_startup_fan", + "type": "function", + "z": "ps_demo_tab", + "name": "fan startup to pumps", + "func": "const cmd = { topic: 'execSequence', payload: { source: 'GUI', action: 'execSequence', parameter: 'startup' } };\nreturn [cmd, cmd, cmd];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 520, + "wires": [ + [ + "pump_a" + ], + [ + "pump_b" + ], + [ + "pump_c" + ] + ] + }, + { + "id": "btn_station_shutdown", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "name": "Stop all pumps", + "label": "Shutdown all", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#ea580c", + "className": "", + "icon": "stop", + "iconPosition": "left", + "payload": "shutdown", + "payloadType": "str", + "topic": "stationShutdown", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 580, + "wires": [ + [ + "station_shutdown_fan" + ] + ] + }, + { + "id": "station_shutdown_fan", + "type": "function", + "z": "ps_demo_tab", + "name": "fan shutdown to pumps", + "func": "const cmd = { topic: 'execSequence', payload: { source: 'GUI', action: 'execSequence', parameter: 'shutdown' } };\nreturn [cmd, cmd, cmd];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 580, + "wires": [ + [ + "pump_a" + ], + [ + "pump_b" + ], + [ + "pump_c" + ] + ] + }, + { + "id": "btn_station_estop", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "name": "EMERGENCY STOP", + "label": "EMERGENCY STOP", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#dc2626", + "className": "", + "icon": "stop_circle", + "iconPosition": "left", + "payload": "estop", + "payloadType": "str", + "topic": "stationEstop", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 640, + "wires": [ + [ + "station_estop_fan" + ] + ] + }, + { + "id": "station_estop_fan", + "type": "function", + "z": "ps_demo_tab", + "name": "fan estop to pumps", + "func": "const cmd = { topic: 'emergencystop', payload: { source: 'GUI', action: 'emergencystop' } };\nreturn [cmd, cmd, cmd];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 640, + "wires": [ + [ + "pump_a" + ], + [ + "pump_b" + ], + [ + "pump_c" + ] + ] + }, + { + "id": "ui_pump_a_state", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A state", + "label": "State", + "format": "{{msg.payload.state}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_mode", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A mode", + "label": "Mode", + "format": "{{msg.payload.mode}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_ctrl", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A ctrl", + "label": "Controller %", + "format": "{{msg.payload.ctrl}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_flow", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A flow", + "label": "Flow (m\u00b3/h)", + "format": "{{msg.payload.flow}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_power", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A power", + "label": "Power (kW)", + "format": "{{msg.payload.power}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_pUp", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A pUp", + "label": "p Upstream (mbar)", + "format": "{{msg.payload.pUp}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_pDn", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump A pDn", + "label": "p Downstream (mbar)", + "format": "{{msg.payload.pDn}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_a_setpoint", + "type": "ui-slider", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "name": "Pump A setpoint", + "label": "Setpoint % (manual mode)", + "tooltip": "", + "order": 1, + "width": "0", + "height": "0", + "passthru": true, + "outs": "end", + "topic": "setpoint_pump_a", + "topicType": "str", + "min": "0", + "max": "100", + "step": "5.0", + "showLabel": true, + "showValue": true, + "labelPosition": "top", + "valuePosition": "left", + "thumbLabel": false, + "iconStart": "", + "iconEnd": "", + "x": 100, + "y": 100, + "wires": [ + [ + "setpoint_to_pump_pump_a" + ] + ] + }, + { + "id": "setpoint_to_pump_pump_a", + "type": "function", + "z": "ps_demo_tab", + "name": "build setpoint cmd for Pump A", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 100, + "wires": [ + [ + "pump_a" + ] + ] + }, + { + "id": "btn_pump_a_startup", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "name": "Pump A startup", + "label": "Startup", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#16a34a", + "className": "", + "icon": "play_arrow", + "iconPosition": "left", + "payload": "startup", + "payloadType": "str", + "topic": "start_pump_a", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 160, + "wires": [ + [ + "start_to_pump_pump_a" + ] + ] + }, + { + "id": "start_to_pump_pump_a", + "type": "function", + "z": "ps_demo_tab", + "name": "build startup for Pump A", + "func": "msg.topic = 'execSequence';\nmsg.payload = { source: 'GUI', action: 'execSequence', parameter: 'startup' };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 160, + "wires": [ + [ + "pump_a" + ] + ] + }, + { + "id": "btn_pump_a_shutdown", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_pump_a", + "name": "Pump A shutdown", + "label": "Shutdown", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#ea580c", + "className": "", + "icon": "stop", + "iconPosition": "left", + "payload": "shutdown", + "payloadType": "str", + "topic": "stop_pump_a", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 220, + "wires": [ + [ + "stop_to_pump_pump_a" + ] + ] + }, + { + "id": "stop_to_pump_pump_a", + "type": "function", + "z": "ps_demo_tab", + "name": "build shutdown for Pump A", + "func": "msg.topic = 'execSequence';\nmsg.payload = { source: 'GUI', action: 'execSequence', parameter: 'shutdown' };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 220, + "wires": [ + [ + "pump_a" + ] + ] + }, + { + "id": "router_p0_pump_a", + "type": "function", + "z": "ps_demo_tab", + "name": "format Pump A port 0", + "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst pU = find('pressure.measured.upstream.');\nconst pD = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n flowNum: flow != null ? Number(flow) : null,\n powerNum: power != null ? Number(power) : null\n};\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1100, + "y": 190, + "wires": [ + [ + "ui_pump_a_state", + "ui_pump_a_mode", + "ui_pump_a_ctrl", + "ui_pump_a_flow", + "ui_pump_a_power", + "ui_pump_a_pUp", + "ui_pump_a_pDn", + "trend_split_pump_a" + ] + ] + }, + { + "id": "trend_split_pump_a", + "type": "function", + "z": "ps_demo_tab", + "name": "emit trend points for Pump A", + "func": "const p = msg.payload || {};\nconst out = [];\nif (p.flowNum != null) out.push({ topic: 'Pump A flow', payload: p.flowNum });\nif (p.powerNum != null) out.push({ topic: 'Pump A power', payload: p.powerNum });\nreturn [out];", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1300, + "y": 250, + "wires": [ + [ + "trend_chart_flow", + "trend_chart_power" + ] + ] + }, + { + "id": "ui_pump_b_state", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B state", + "label": "State", + "format": "{{msg.payload.state}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_mode", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B mode", + "label": "Mode", + "format": "{{msg.payload.mode}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_ctrl", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B ctrl", + "label": "Controller %", + "format": "{{msg.payload.ctrl}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_flow", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B flow", + "label": "Flow (m\u00b3/h)", + "format": "{{msg.payload.flow}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_power", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B power", + "label": "Power (kW)", + "format": "{{msg.payload.power}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_pUp", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B pUp", + "label": "p Upstream (mbar)", + "format": "{{msg.payload.pUp}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_pDn", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump B pDn", + "label": "p Downstream (mbar)", + "format": "{{msg.payload.pDn}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_b_setpoint", + "type": "ui-slider", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "name": "Pump B setpoint", + "label": "Setpoint % (manual mode)", + "tooltip": "", + "order": 1, + "width": "0", + "height": "0", + "passthru": true, + "outs": "end", + "topic": "setpoint_pump_b", + "topicType": "str", + "min": "0", + "max": "100", + "step": "5.0", + "showLabel": true, + "showValue": true, + "labelPosition": "top", + "valuePosition": "left", + "thumbLabel": false, + "iconStart": "", + "iconEnd": "", + "x": 100, + "y": 420, + "wires": [ + [ + "setpoint_to_pump_pump_b" + ] + ] + }, + { + "id": "setpoint_to_pump_pump_b", + "type": "function", + "z": "ps_demo_tab", + "name": "build setpoint cmd for Pump B", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 420, + "wires": [ + [ + "pump_b" + ] + ] + }, + { + "id": "btn_pump_b_startup", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "name": "Pump B startup", + "label": "Startup", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#16a34a", + "className": "", + "icon": "play_arrow", + "iconPosition": "left", + "payload": "startup", + "payloadType": "str", + "topic": "start_pump_b", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 480, + "wires": [ + [ + "start_to_pump_pump_b" + ] + ] + }, + { + "id": "start_to_pump_pump_b", + "type": "function", + "z": "ps_demo_tab", + "name": "build startup for Pump B", + "func": "msg.topic = 'execSequence';\nmsg.payload = { source: 'GUI', action: 'execSequence', parameter: 'startup' };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 480, + "wires": [ + [ + "pump_b" + ] + ] + }, + { + "id": "btn_pump_b_shutdown", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_pump_b", + "name": "Pump B shutdown", + "label": "Shutdown", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#ea580c", + "className": "", + "icon": "stop", + "iconPosition": "left", + "payload": "shutdown", + "payloadType": "str", + "topic": "stop_pump_b", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 540, + "wires": [ + [ + "stop_to_pump_pump_b" + ] + ] + }, + { + "id": "stop_to_pump_pump_b", + "type": "function", + "z": "ps_demo_tab", + "name": "build shutdown for Pump B", + "func": "msg.topic = 'execSequence';\nmsg.payload = { source: 'GUI', action: 'execSequence', parameter: 'shutdown' };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 540, + "wires": [ + [ + "pump_b" + ] + ] + }, + { + "id": "router_p0_pump_b", + "type": "function", + "z": "ps_demo_tab", + "name": "format Pump B port 0", + "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst pU = find('pressure.measured.upstream.');\nconst pD = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n flowNum: flow != null ? Number(flow) : null,\n powerNum: power != null ? Number(power) : null\n};\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1100, + "y": 510, + "wires": [ + [ + "ui_pump_b_state", + "ui_pump_b_mode", + "ui_pump_b_ctrl", + "ui_pump_b_flow", + "ui_pump_b_power", + "ui_pump_b_pUp", + "ui_pump_b_pDn", + "trend_split_pump_b" + ] + ] + }, + { + "id": "trend_split_pump_b", + "type": "function", + "z": "ps_demo_tab", + "name": "emit trend points for Pump B", + "func": "const p = msg.payload || {};\nconst out = [];\nif (p.flowNum != null) out.push({ topic: 'Pump B flow', payload: p.flowNum });\nif (p.powerNum != null) out.push({ topic: 'Pump B power', payload: p.powerNum });\nreturn [out];", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1300, + "y": 570, + "wires": [ + [ + "trend_chart_flow", + "trend_chart_power" + ] + ] + }, + { + "id": "ui_pump_c_state", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C state", + "label": "State", + "format": "{{msg.payload.state}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_mode", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C mode", + "label": "Mode", + "format": "{{msg.payload.mode}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_ctrl", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C ctrl", + "label": "Controller %", + "format": "{{msg.payload.ctrl}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_flow", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C flow", + "label": "Flow (m\u00b3/h)", + "format": "{{msg.payload.flow}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_power", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C power", + "label": "Power (kW)", + "format": "{{msg.payload.power}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_pUp", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C pUp", + "label": "p Upstream (mbar)", + "format": "{{msg.payload.pUp}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_pDn", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "order": 1, + "width": "0", + "height": "0", + "name": "Pump C pDn", + "label": "p Downstream (mbar)", + "format": "{{msg.payload.pDn}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_pump_c_setpoint", + "type": "ui-slider", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "name": "Pump C setpoint", + "label": "Setpoint % (manual mode)", + "tooltip": "", + "order": 1, + "width": "0", + "height": "0", + "passthru": true, + "outs": "end", + "topic": "setpoint_pump_c", + "topicType": "str", + "min": "0", + "max": "100", + "step": "5.0", + "showLabel": true, + "showValue": true, + "labelPosition": "top", + "valuePosition": "left", + "thumbLabel": false, + "iconStart": "", + "iconEnd": "", + "x": 100, + "y": 740, + "wires": [ + [ + "setpoint_to_pump_pump_c" + ] + ] + }, + { + "id": "setpoint_to_pump_pump_c", + "type": "function", + "z": "ps_demo_tab", + "name": "build setpoint cmd for Pump C", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 740, + "wires": [ + [ + "pump_c" + ] + ] + }, + { + "id": "btn_pump_c_startup", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "name": "Pump C startup", + "label": "Startup", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#16a34a", + "className": "", + "icon": "play_arrow", + "iconPosition": "left", + "payload": "startup", + "payloadType": "str", + "topic": "start_pump_c", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 800, + "wires": [ + [ + "start_to_pump_pump_c" + ] + ] + }, + { + "id": "start_to_pump_pump_c", + "type": "function", + "z": "ps_demo_tab", + "name": "build startup for Pump C", + "func": "msg.topic = 'execSequence';\nmsg.payload = { source: 'GUI', action: 'execSequence', parameter: 'startup' };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 800, + "wires": [ + [ + "pump_c" + ] + ] + }, + { + "id": "btn_pump_c_shutdown", + "type": "ui-button", + "z": "ps_demo_tab", + "group": "ui_grp_pump_c", + "name": "Pump C shutdown", + "label": "Shutdown", + "order": 1, + "width": "0", + "height": "0", + "tooltip": "", + "color": "#ffffff", + "bgcolor": "#ea580c", + "className": "", + "icon": "stop", + "iconPosition": "left", + "payload": "shutdown", + "payloadType": "str", + "topic": "stop_pump_c", + "topicType": "str", + "buttonType": "default", + "x": 100, + "y": 860, + "wires": [ + [ + "stop_to_pump_pump_c" + ] + ] + }, + { + "id": "stop_to_pump_pump_c", + "type": "function", + "z": "ps_demo_tab", + "name": "build shutdown for Pump C", + "func": "msg.topic = 'execSequence';\nmsg.payload = { source: 'GUI', action: 'execSequence', parameter: 'shutdown' };\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 860, + "wires": [ + [ + "pump_c" + ] + ] + }, + { + "id": "router_p0_pump_c", + "type": "function", + "z": "ps_demo_tab", + "name": "format Pump C port 0", + "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst pU = find('pressure.measured.upstream.');\nconst pD = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n flowNum: flow != null ? Number(flow) : null,\n powerNum: power != null ? Number(power) : null\n};\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1100, + "y": 830, + "wires": [ + [ + "ui_pump_c_state", + "ui_pump_c_mode", + "ui_pump_c_ctrl", + "ui_pump_c_flow", + "ui_pump_c_power", + "ui_pump_c_pUp", + "ui_pump_c_pDn", + "trend_split_pump_c" + ] + ] + }, + { + "id": "trend_split_pump_c", + "type": "function", + "z": "ps_demo_tab", + "name": "emit trend points for Pump C", + "func": "const p = msg.payload || {};\nconst out = [];\nif (p.flowNum != null) out.push({ topic: 'Pump C flow', payload: p.flowNum });\nif (p.powerNum != null) out.push({ topic: 'Pump C power', payload: p.powerNum });\nreturn [out];", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1300, + "y": 890, + "wires": [ + [ + "trend_chart_flow", + "trend_chart_power" + ] + ] + }, + { + "id": "trend_chart_flow", + "type": "ui-chart", + "z": "ps_demo_tab", + "group": "ui_grp_trend", + "name": "Flow per pump (m\u00b3/h)", + "label": "Flow per pump", + "order": 1, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "", + "xAxisType": "time", + "xAxisTimeFormat": "auto", + "yAxisLabel": "", + "ymin": "", + "ymax": "", + "action": "append", + "pointShape": "circle", + "pointRadius": 2, + "showLegend": true, + "removeOlder": "10", + "removeOlderUnit": "60", + "removeOlderPoints": "200", + "colors": [], + "textColor": [], + "textColorDefault": true, + "width": "0", + "height": "0", + "className": "", + "wires": [ + [] + ] + }, + { + "id": "trend_chart_power", + "type": "ui-chart", + "z": "ps_demo_tab", + "group": "ui_grp_trend", + "name": "Power per pump (kW)", + "label": "Power per pump", + "order": 1, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "", + "xAxisType": "time", + "xAxisTimeFormat": "auto", + "yAxisLabel": "", + "ymin": "", + "ymax": "", + "action": "append", + "pointShape": "circle", + "pointRadius": 2, + "showLegend": true, + "removeOlder": "10", + "removeOlderUnit": "60", + "removeOlderPoints": "200", + "colors": [], + "textColor": [], + "textColorDefault": true, + "width": "0", + "height": "0", + "className": "", + "wires": [ + [] + ] + }, + { + "id": "ps_to_dashboard", + "type": "function", + "z": "ps_demo_tab", + "name": "format PS port 0 for dashboard", + "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst lvl = find('level.predicted.');\nconst vol = find('volume.predicted.');\nconst qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\nconst qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\nmsg.payload = {\n level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n volume: vol != null ? Number(vol).toFixed(1) + ' m\u00b3' : 'n/a',\n qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n state: c.state || c.direction || 'idle',\n netNum: (qIn != null && qOut != null) ? (Number(qIn) - Number(qOut)) * 3600 : null\n};\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1700, + "y": 700, + "wires": [ + [ + "ui_ps_level", + "ui_ps_volume", + "ui_ps_qin", + "ui_ps_qout", + "ui_ps_state" + ] + ] + }, + { + "id": "ui_ps_state", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "order": 1, + "width": "0", + "height": "0", + "name": "PS state", + "label": "Basin state", + "format": "{{msg.payload.state}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_ps_level", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "order": 1, + "width": "0", + "height": "0", + "name": "PS level", + "label": "Basin level", + "format": "{{msg.payload.level}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_ps_volume", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "order": 1, + "width": "0", + "height": "0", + "name": "PS volume", + "label": "Basin volume", + "format": "{{msg.payload.volume}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_ps_qin", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "order": 1, + "width": "0", + "height": "0", + "name": "PS Qin", + "label": "Inflow", + "format": "{{msg.payload.qIn}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "ui_ps_qout", + "type": "ui-text", + "z": "ps_demo_tab", + "group": "ui_grp_station", + "order": 1, + "width": "0", + "height": "0", + "name": "PS Qout", + "label": "Pumped out", + "format": "{{msg.payload.qOut}}", + "layout": "row-left", + "style": false, + "font": "", + "fontSize": 14, + "color": "#000000", + "wires": [] + }, + { + "id": "setup_mgc_scaling", + "type": "inject", + "z": "ps_demo_tab", + "name": "setup: MGC scaling=absolute", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "absolute", + "vt": "str" + } + ], + "topic": "setScaling", + "payload": "absolute", + "payloadType": "str", + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "x": 100, + "y": 800, + "wires": [ + [ + "mgc_pumps" + ] + ] + }, + { + "id": "setup_mgc_mode", + "type": "inject", + "z": "ps_demo_tab", + "name": "setup: MGC mode=optimalcontrol", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "optimalcontrol", + "vt": "str" + } + ], + "topic": "setMode", + "payload": "optimalcontrol", + "payloadType": "str", + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "x": 100, + "y": 840, + "wires": [ + [ + "mgc_pumps" + ] + ] + }, + { + "id": "setup_pumps_mode", + "type": "inject", + "z": "ps_demo_tab", + "name": "setup: pumps mode=auto", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "auto", + "vt": "str" + } + ], + "topic": "setMode", + "payload": "auto", + "payloadType": "str", + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "x": 100, + "y": 880, + "wires": [ + [ + "mode_setup_fan" + ] + ] + }, + { + "id": "mode_setup_fan", + "type": "function", + "z": "ps_demo_tab", + "name": "fan setup mode to pumps", + "func": "msg.topic = 'setMode';\nreturn [msg, msg, msg];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 880, + "wires": [ + [ + "pump_a" + ], + [ + "pump_b" + ], + [ + "pump_c" + ] + ] + }, + { + "id": "setup_pumps_startup", + "type": "inject", + "z": "ps_demo_tab", + "name": "setup: pumps startup", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}", + "vt": "json" + } + ], + "topic": "execSequence", + "payload": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}", + "payloadType": "json", + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "4", + "x": 100, + "y": 920, + "wires": [ + [ + "startup_setup_fan" + ] + ] + }, + { + "id": "startup_setup_fan", + "type": "function", + "z": "ps_demo_tab", + "name": "fan startup to pumps", + "func": "return [msg, msg, msg];", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 320, + "y": 920, + "wires": [ + [ + "pump_a" + ], + [ + "pump_b" + ], + [ + "pump_c" + ] + ] + }, + { + "id": "setup_random_on", + "type": "inject", + "z": "ps_demo_tab", + "name": "setup: random demand ON", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "on", + "vt": "str" + } + ], + "topic": "randomToggle", + "payload": "on", + "payloadType": "str", + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "5", + "x": 100, + "y": 960, + "wires": [ + [ + "random_state" + ] + ] + } +] diff --git a/nodes/generalFunctions b/nodes/generalFunctions index 43f6906..29b78a3 160000 --- a/nodes/generalFunctions +++ b/nodes/generalFunctions @@ -1 +1 @@ -Subproject commit 43f69066af9a088c24fce961b71777df6e4b184a +Subproject commit 29b78a3f9b432985425b9713f3a3988749a89c16