# Reference — Examples ![code-ref](https://img.shields.io/badge/code--ref-b20a573-blue) > [!NOTE] > Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. > > Every example flow shipped under `nodes/valveGroupControl/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/valveGroupControl/examples/`. --- ## Shipped examples | File | Tier | Dependencies | What it shows | |:---|:---:|:---|:---| | `basic.flow.json` | 1 | EVOLV only | Inject `data.totalFlow` + 2 `valve` children registered as the group. No parent. Verifies Kv-share split and residual reconciliation. | | `integration.flow.json` | 2 | EVOLV only | VGC + 2 valves + one upstream `rotatingMachine` source. Source's `flow.predicted.*` events drive the group; fluid-contract aggregation resolves to `liquid`. | | `edge.flow.json` | 3 | EVOLV only | Edge cases: no valves available, residual non-convergence, cascaded VGC as upstream source. Currently a structural placeholder — TODO consult `examples/edge.flow.json` for actual scenarios. | > [!IMPORTANT] > The Tier-1 / Tier-2 / Tier-3 naming convention (`01 - .json`) used by `rotatingMachine` has **not yet been applied** to VGC. Filenames are still the legacy `basic`/`integration`/`edge` triad. Migration tracked in `.agents/improvements/IMPROVEMENTS_BACKLOG.md`. When renaming, update `examples/README.md` in the same commit. --- ## Loading a flow ### Via the editor 1. Open the Node-RED editor at `http://localhost:1880`. 2. Menu → Import → drag the JSON file. 3. Click Deploy. ### Via the Admin API ```bash curl -X POST -H 'Content-Type: application/json' \ --data @"nodes/valveGroupControl/examples/basic.flow.json" \ http://localhost:1880/flows ``` --- ## Example: basic.flow.json > [!IMPORTANT] > **TODO: not yet validated against live Node-RED.** Steps below are inferred from `CONTRACT.md` + the topic registry. Consult `examples/basic.flow.json` directly for exact node ids and inject payloads. Screenshot needed once validated — save under `wiki/_partial-screenshots/valveGroupControl/01-basic-editor.png`. Single VGC with 2 `valve` children. Demonstrates: 1. Wire each valve's Port 2 to VGC's input so `child.register` arrives automatically on deploy. 2. Inject `data.totalFlow` with payload `{ value: 80, unit: "m3/h", position: "atEquipment", variant: "measured" }`. VGC runs `calcValveFlows`: - Splits 80 m³/h by Kv share across the 2 valves. - Each valve's `updateFlow('predicted', share, 'downstream')` push. - Re-reads each valve's accepted `flow.predicted.downstream`. - Residual loop runs up to `maxPasses: 2` until `|residual| < 0.001`. 3. Watch Port 0 debug: `atEquipment_predicted_flow` settles at the assigned total; `deltaMax_predicted_pressure` updates as each valve emits `deltaPChange`. 4. Toggle one valve to `off` — the next `data.totalFlow` (or tick) routes 100% to the remaining valve. 5. Set the offlined valve back to operational — the next tick re-includes it in the split. ### Try the residual pass After the group settles at `80 m³/h`: 1. Inject `data.totalFlow = 200` (a value beyond aggregate Kv capacity). Watch `lastFlowSolve.residual` — it stays large because the valves cap at their accept limits. `flow.predicted.atEquipment` will read the sum of caps, not 200. 2. Inject `data.totalFlow = 0`. Every valve receives `updateFlow('predicted', 0, 'downstream')`; `maxDeltaP` collapses to 0. --- ## Example: integration.flow.json > [!IMPORTANT] > **TODO: not yet validated.** Screenshot needed once validated — save under `wiki/_partial-screenshots/valveGroupControl/02-integration-editor.png`. VGC + 2 valves + 1 upstream `rotatingMachine`. Demonstrates: - Source registration: the rotatingMachine's `child.register` (softwareType `rotatingmachine` → canonical `machine`) lands on VGC; `_registerSource` subscribes to `flow.predicted.downstream`, `flow.measured.downstream`, etc. - Source-driven flow: a pump-state change emits `flow.predicted.downstream`; VGC's handler converts to `updateFlow('predicted', value, 'atEquipment', unit)` and re-runs `calcValveFlows`. - Fluid-contract resolution: the pump's `getFluidContract()` (or fallback `DEFAULT_SOURCE_SERVICE_TYPE.rotatingmachine = 'liquid'`) produces `serviceType: 'liquid'`; `fluidContract` resolves to `{status: 'resolved', serviceType: 'liquid'}`. --- ## Example: edge.flow.json > [!IMPORTANT] > **TODO: structural placeholder.** Consult the JSON directly for the scenarios it currently exercises. The edge scenarios this node ought to test (per `CONTRACT.md` + source review): > > - **No available valves** — all in `off` / `maintenance`. Expected: status badge `'No valves'` red, every valve pushed `0`, `lastFlowSolve.assignedTotal = 0`. > - **Residual non-convergence** — valve curve where Kv-share is a bad first estimate. Expected: loop exits after `maxPasses: 2` with non-zero residual; assignedTotal < target; behaviour graceful. > - **Cascaded VGC** — upstream source is another VGC. Expected: source registered, `flow.*` events bound, `getFluidContract()` propagated up. **Not exercised in production.** > - **Conflicting fluid contracts** — two sources advertise `liquid` vs `gas`. Expected: `fluidContract.status = 'conflict'`; `fluidContractChange` event emitted. --- ## Docker compose snippet To bring up Node-RED + InfluxDB with EVOLV nodes pre-loaded: ```yaml # docker-compose.yml (extract) services: nodered: build: ./docker/nodered ports: ['1880:1880'] volumes: - ./docker/nodered/data:/data/evolv influxdb: image: influxdb:2.7 ports: ['8086:8086'] ``` Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/docker-compose.yml). --- ## Debug recipes | Symptom | First thing to check | Where to look | |:---|:---|:---| | All valves receive `assigned flow = 0` | `getAvailableValves()` returns empty list: check every valve's state (`off` / `maintenance` excludes), `currentMode !== 'maintenance'`, and `kv > 0`. | `src/groupOps/flowDistribution.isValveAvailable` | | Residual never converges | Pathological valve curve: Kv-share is a bad first estimate. Check `vgc.lastFlowSolve.residual` and `.passes` — if `passes === maxPasses` and residual is large, the loop ran out. Raise `flowReconciliation.maxPasses` (no editor field yet — TODO Phase 7). | `src/groupOps/flowDistribution.solveFlowDistribution` | | Group `maxDeltaP` stale | Child `deltaPChange` not subscribed: valve emitter not exposed via `child.emitter`, or valve never registered. | `src/specificClass._bindValveEvents` | | Service-type stays `unknown` | No upstream source registered, or all advertise unknown type. Check `vgc.sources` — if empty, no Port-2 child.register reached VGC. | `src/sources/fluidContract.refreshFluidContract` | | `data.totalFlow` silently ignored | Mode rejects the source id: check `mode.allowedSources` for the current mode. `maintenance` rejects every source. Warn in log: `Source '' is not valid for mode ''.` | `src/specificClass.isValidSourceForMode` | | `set.position` has no effect | Known: handler is a debug-logged no-op pending Phase 7. See [Limitations](Reference-Limitations#set-position-is-a-no-op). | `src/commands/handlers.setPosition` | | `set.reconcileInterval = 0` (or negative) silently does nothing | The handler validates `Number.isFinite(nextSec) && nextSec > 0`. Otherwise warns and returns. | `src/commands/handlers.setReconcileInterval` | | Two emitters fight on the same flow channel (one source publishes `atEquipment`, another publishes `atequipment`) | Both event names are wired; the **last write wins** on each `updateFlow`. There is no source-priority logic. | `src/sources/fluidContract.SOURCE_FLOW_EVENTS` | | Cascaded VGC not propagating flow | Accepted by router, but no tests — treat as experimental. | `src/sources/fluidContract.SOURCE_SOFTWARE_TYPES` | | Output port-0 key shape differs from rotatingMachine's | VGC uses `__` (same as MGC) — the inverse of rotatingMachine's `...`. Don't mix. | `src/io/output.getOutput` | > Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. --- ## Output coverage (TODO) > [!IMPORTANT] > Per `.claude/rules/output-coverage.md`: every output on every layer needs a manifest entry + populated + degraded test. **`test/_output-manifest.md` does not yet exist** for VGC. Backfill tracked in `.agents/improvements/IMPROVEMENTS_BACKLOG.md`. > > Minimum coverage to land before declaring trial-ready: > > - Port 0 / 1 keys: `mode`, `maxDeltaP`, `atEquipment_predicted_flow`, `atEquipment_measured_flow`, `deltaMax_predicted_pressure`. Each tested in populated AND degraded states. > - Port 2: `child.register` payload shape. > - Example flow function-node outputs: each `outputs > 1` fan-out enumerated; verify no `payload: null` literals (lint via `npm run lint:flow-outputs`). --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Contracts](Reference-Contracts) | Topic + config + child filters | | [Reference — Architecture](Reference-Architecture) | Code map, flow-distribution loop, source aggregation | | [Reference — Limitations](Reference-Limitations) | Known issues and open questions | | [machineGroupControl — Examples](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Reference-Examples) | Sibling group-control demo flows | | [valve wiki](https://gitea.wbd-rd.nl/RnD/valve/wiki/Home) | The child node VGC coordinates | | [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where valveGroupControl fits in a larger plant |