Files
valveGroupControl/wiki/Home.md
znetsixe 618ad27e03 docs(wiki): rewrite Home.md to 14-section visual-first template
Fixes section ordering (11-8-9-10 scramble + duplicate §11), updates
banner hash to ef34c82, corrects §2 diagram (set.position → updateFlow,
evt.deltaPChange → positionChange/deltaPChange), upgrades capability
matrix to ⚠️ for set.position and cascaded VGC, rewrites §9 to reflect
the actual editor form fields (name/format/position/logger), and
replaces the orphaned "Distribution loop" duplicate-§11 with a proper
§10. AUTOGEN markers verified intact via npm run wiki:all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:05:17 +02:00

261 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# valveGroupControl
> **Reflects code as of `ef34c82` · regenerated `2026-05-11` via `npm run wiki:all`**
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
## 1. What this node is
**valveGroupControl** (VGC) is an S88 Unit coordinator for a group of `valve` children. It distributes a group-level flow target across registered valves proportional to their Kv rating, runs a residual-reconciliation pass to absorb per-valve acceptance limits, aggregates group max delta-P, and reconciles upstream fluid-contract advertisements into a single group-level service-type view.
## 2. Position in the platform
```mermaid
flowchart LR
src[machine / MGC / PS<br/>upstream source]:::unit -.flow.predicted.-> vgc[valveGroupControl<br/>Unit]:::unit
vgc -->|updateFlow predicted| v1[valve A]:::equip
vgc -->|updateFlow predicted| v2[valve B]:::equip
v1 -->|positionChange| vgc
v2 -->|positionChange| vgc
v1 -->|deltaPChange| vgc
v2 -->|deltaPChange| vgc
vgc -->|child.register| parent[upstream parent]:::unit
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
```
S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| Proportional flow distribution by Kv | ✅ | `groupOps/flowDistribution.js`. |
| Two-pass residual reconciliation | ✅ | Default `maxPasses: 2`, `residualTolerance: 0.001`. |
| Periodic tick re-balance | ✅ | `tick()` calls `calcValveFlows()`; `set.reconcileInterval` re-tunes interval. |
| Group `maxDeltaP` aggregation | ✅ | Recomputed on any child `deltaPChange` event. |
| Upstream fluid-contract aggregation | ✅ | `sources/fluidContract.js`; emits `fluidContractChange`. |
| Group-wide sequence dispatch | ✅ | `cmd.execSequence``executeSequence` → per-state `transitionToState`. |
| Emergency stop | ✅ | `cmd.emergencyStop``emergencystop` sequence on all valves. |
| Per-valve positional override | ⚠️ | `set.position` is a debug-logged **no-op** pending Phase 7. See §14. |
| Multi-source aggregation | ✅ | Multiple machines / MGCs / PSs may register as upstream sources. |
| Cascaded VGC as upstream source | ⚠️ | Accepted by router but not exercised in production; test coverage absent. |
## 4. Code map
```mermaid
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass, commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["ValveGroupControl.configure()<br/>router.onRegister: valve + sources<br/>tick() → calcValveFlows()"]
end
subgraph concerns["src/ concern modules"]
gops["groupOps/<br/>flowDistribution + calcMaxDeltaP"]
srcs["sources/<br/>fluidContract registration"]
cmds["commands/<br/>topic registry + handlers"]
io["io/<br/>getOutput + getStatusBadge"]
end
nc --> sc
sc --> gops
sc --> srcs
sc --> io
nc --> cmds
```
| Module | Owns | Read first if you're changing… |
|---|---|---|
| `groupOps/` | Kv-share solver, residual pass, max-deltaP aggregation | How the group divides flow between valves. |
| `sources/` | Upstream-source registration, flow-event subscription, fluid-contract reconciliation | Service-type aggregation, source event wiring. |
| `commands/` | Input-topic registry + per-topic handlers | New input topics or payload validation rules. |
| `io/` | Port-0 output shape + status badge composition | What lands on the wire, badge text. |
## 5. Topic contract
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
The **Unit** column reflects the descriptor's `units: { measure, default }` declaration, rendered as `<measure> (default <unit>)`. Topics without a `units` field show `—`. The **Effect** column is sourced from the descriptor's `description` field.
<!-- BEGIN AUTOGEN: topic-contract -->
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
| `set.mode` | `setMode` | `string` | — | Switch the valve group between auto / manual control modes. |
| `set.position` | `setpoint` | `any` | — | Set the group-level valve position (currently a no-op pending Phase 7). |
| `child.register` | `registerChild` | `string` | — | Register a child valve with this group. |
| `cmd.execSequence` | `execSequence` | `object` | — | Run a group-wide sequence (startup / shutdown / emergencystop). |
| `data.totalFlow` | `totalFlowChange` | `any` | — | Notify the group that the total flow setpoint has changed. |
| `cmd.emergencyStop` | `emergencyStop`, `emergencystop` | `any` | — | Trigger an emergency stop across all valves in the group. |
| `set.reconcileInterval` | `setReconcileInterval` | `any` | — | Update the reconciliation interval (seconds). |
<!-- END AUTOGEN: topic-contract -->
## 6. Child registration
```mermaid
flowchart LR
subgraph kids["accepted children (softwareType)"]
v["valve"]:::equip
src["machine / rotatingmachine /<br/>machinegroup / pumpingstation /<br/>valvegroupcontrol"]:::unit
end
v -->|positionChange| handler1[onPositionChange<br/>→ calcValveFlows]
v -->|deltaPChange| handler2[onDeltaPChange<br/>→ calcMaxDeltaP]
src -->|flow.predicted.*<br/>flow.measured.*| handler3[updateFlow<br/>atEquipment]
src -->|fluidContractChange| handler4[sources.refresh<br/>aggregate contract]
classDef equip fill:#86bbdd,color:#000
classDef unit fill:#50a8d9,color:#000
```
| softwareType | onRegister side-effect | Subscribed events |
|---|---|---|
| `valve` | Stored in `this.valves[id]`; binds `positionChange` (via `child.state.emitter`) + `deltaPChange` (via `child.emitter`); triggers `calcValveFlows` + `calcMaxDeltaP`. | `positionChange`, `deltaPChange`. |
| `machine` / `rotatingmachine` | Stored as upstream source; reads `getFluidContract()` or infers `liquid` by default. | `flow.predicted.*`, `flow.measured.*`, `fluidContractChange`. |
| `machinegroup` / `machinegroupcontrol` | Same as machine. | Same as machine. |
| `pumpingstation` | Same as machine. | Same as machine. |
| `valvegroupcontrol` | Cascaded VGC; accepted by router. Not exercised in production. | Same as machine. |
## 7. Lifecycle — what one tick / event does
```mermaid
sequenceDiagram
participant src as upstream source
participant vgc as VGC
participant v1 as valve A
participant v2 as valve B
participant out as Port-0
src->>vgc: flow.predicted.downstream (m³/h)
vgc->>vgc: updateFlow('predicted', value, 'atEquipment')
vgc->>vgc: calcValveFlows()
Note over vgc: solveFlowDistribution<br/>by Kv share (≤ maxPasses)
vgc->>v1: updateFlow('predicted', shareA, 'downstream')
vgc->>v2: updateFlow('predicted', shareB, 'downstream')
v1-->>vgc: positionChange / deltaPChange
v2-->>vgc: positionChange / deltaPChange
vgc->>vgc: calcMaxDeltaP
vgc->>vgc: notifyOutputChanged()
vgc->>out: msg{topic, payload (delta-compressed)}
```
## 8. Data model — `getOutput()`
What lands on Port 0. Composed in `io/output.getOutput()`, then delta-compressed by `outputUtils.formatMsg`.
<!-- BEGIN AUTOGEN: data-model -->
| Key | Type | Unit | Sample |
|---|---|---|---|
| `maxDeltaP` | number | — | `0` |
| `mode` | string | — | `"auto"` |
<!-- END AUTOGEN: data-model -->
Measurement-derived keys follow the `<position>_<variant>_<type>` shape and are emitted only when the container holds a finite value.
| Example key | Unit | Description |
|---|---|---|
| `atEquipment_predicted_flow` | m³/h | Total group predicted flow (sum of per-valve assigned flows). |
| `atEquipment_measured_flow` | m³/h | Total group measured flow (from upstream source). |
| `deltaMax_predicted_pressure` | mbar | Max delta-P across registered valves. |
> Delta compression: only changed fields are sent per tick. Consumers must cache and merge. See `outputUtils.formatMsg`.
## 9. Configuration — editor form ↔ config keys
```mermaid
flowchart TB
subgraph editor["Node-RED editor form (vgc.html)"]
f1[Name]
f2[Process output format]
f3[Database output format]
f4[Position vs parent]
f5[Logger / log level]
end
subgraph config["Domain config (runtime / schema)"]
c1[general.name]
c2[output.process]
c3[output.dbase]
c4[functionality.positionVsParent]
c5[logging.enableLog / logLevel]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
```
| Form field | Config key | Default | Where used |
|---|---|---|---|
| Name | `general.name` | `""` | Node label, InfluxDB tag, status badge. |
| Process output format | `output.process` | `"process"` | `outputUtils.formatMsg` Port-0 formatter. |
| Database output format | `output.dbase` | `"influxdb"` | `outputUtils.formatMsg` Port-1 formatter. |
| Position vs parent | `functionality.positionVsParent` | `""` | Child registration Port-2 message; read by upstream parent. |
| Enable log / log level | `logging.enableLog`, `logging.logLevel` | `false` / `"error"` | Logger verbosity. Never ship `debug` in demos. |
> Domain-level config (`mode.current`, `sequences`, `mode.allowedSources`, `flowReconciliation`) is loaded from the node's schema defaults and updated at runtime via `set.mode` / `set.reconcileInterval`. These fields are not exposed in the editor form.
## 10. Flow-distribution loop (replaces state chart)
VGC has no FSM of its own — state semantics belong to the child valves. The core coordination loop is the Kv-share solver described below.
```mermaid
sequenceDiagram
participant tick as adapter tick / incoming event
participant vgc as VGC
participant solver as solveFlowDistribution
participant valves as valve children
tick->>vgc: calcValveFlows()
vgc->>vgc: read flow.measured (or predicted) .atEquipment
vgc->>solver: target=totalFlow, entries=availableValves
loop ≤ maxPasses while |residual| > tolerance
solver->>valves: updateFlow share by (Kv / totalKv)
valves-->>solver: accepted flow (read back from child.measurements)
solver->>solver: residual = target sum(accepted)
end
solver-->>vgc: { flowsById, residual, passes }
vgc->>vgc: write flow.predicted.atEquipment = assignedTotal
vgc->>vgc: calcMaxDeltaP()
vgc->>vgc: notifyOutputChanged()
```
A valve is **available** if: `state ≠ off|maintenance`, `mode ≠ maintenance`, and `kv > 0`. Unavailable valves receive `updateFlow('predicted', 0)` and are excluded from the solver.
## 11. Examples
| Tier | File | What it shows | Mandatory? |
|---|---|---|---|
| Basic | `examples/basic.flow.json` | Inject `data.totalFlow` + 2 valves (no parent) | ✅ |
| Integration | `examples/integration.flow.json` | VGC + valves + upstream rotatingMachine | ✅ |
| Edge | `examples/edge.flow.json` | Edge cases: no valves, residual non-convergence, cascaded VGC | ⭕ |
Tier-1 / Tier-2 / Tier-3 naming scheme pending when existing flows are validated on live Node-RED. Screenshots under `wiki/_partial-screenshots/valveGroupControl/` when produced.
## 12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| All valves receive `assigned flow = 0` | `getAvailableValves()` returns empty list; check Kv > 0 and child state ≠ `off` / `maintenance`. | `groupOps/flowDistribution.isValveAvailable` |
| Residual never converges | `flowReconciliation.maxPasses` too low for valve curve shape; check `lastFlowSolve.residual`. | `groupOps/flowDistribution.js` |
| Group `maxDeltaP` stale | Child `deltaPChange` not subscribed; valve emitter not exposed via `child.emitter`. | `specificClass._bindValveEvents` |
| Service-type stays `unknown` | No upstream source registered, or all advertise unknown type. | `sources/fluidContract.refreshFluidContract` |
| `data.totalFlow` silently ignored | Mode rejects the source id; check `mode.allowedSources` for the current mode. | `specificClass.isValidSourceForMode` |
| `set.position` has no effect | Known limitation — handler is a no-op. See §14. | `commands/handlers.setPosition` |
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging.
## 13. When you would NOT use this node
- Use `valve` directly when you have a **single** valve under an upstream parent. VGC adds coordination overhead for no benefit with one child.
- Do NOT use VGC to coordinate **series** valves — the Kv-share solver assumes parallel branches sharing a common header pressure.
- Skip VGC when the upstream source already publishes **per-branch flow setpoints**; route those directly to each valve instead.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | `set.position` is a debug-logged **no-op** — per-valve positional override is reserved, pending Phase 7 topic standardisation of valve setpoint payloads. The handler in `commands/handlers.setPosition` intentionally does nothing. | `CONTRACT.md §Inputs`; `OPEN_QUESTIONS.md` Phase 7 |
| 2 | Residual solver assumes Kv share is a valid first estimate. Pathological valve curves (e.g. very non-linear Kv vs position) may need more passes than the default `maxPasses: 2` to reach `residualTolerance`. | `groupOps/flowDistribution.js` |
| 3 | Cascaded `valvegroupcontrol` registration (VGC as upstream source of another VGC) is accepted by the router but has no test coverage and is not exercised in production. | `CONTRACT.md §Children` |