Files
valveGroupControl/wiki/Reference-Contracts.md
znetsixe 91f98414d1 fix(commands): point set.mode description at the schema enum
Old description said "auto / manual" but the schema declares four modes
(auto, virtualControl, fysicalControl, maintenance). New description
enumerates the allowed values and refers readers to the schema as the
source of truth. wiki-gen regenerated Reference-Contracts.md to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:05:40 +02:00

258 lines
13 KiB
Markdown

# Reference &mdash; Contracts
![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.
>
> Full topic contract, configuration schema, and child-registration filters for `valveGroupControl`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/valveGroupControl.json`.
>
> For an intuitive overview, return to the [Home](Home).
---
## Topic contract
The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire.
<!-- BEGIN AUTOGEN: topic-contract -->
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
| `set.mode` | `setMode` | `string` | — | Switch the operating mode. Allowed: `auto`, `virtualControl`, `fysicalControl`, `maintenance` (schema-validated in `valveGroupControl.json``mode.current`). |
| `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 -->
### Mode / source allow-lists
A topic that survives the registry still passes through `flowController` &rarr; `handleInput`, which enforces:
```js
if (!host.isValidSourceForMode(source, host.currentMode)) {
this.logger.warn(`Source '${source}' is not valid for mode '${this.currentMode}'.`);
return { status: false, feedback: ... };
}
```
Defaults from the schema:
| Mode | `allowedActions` | `allowedSources` |
|:---|:---|:---|
| `auto` | `statusCheck, execSequence, emergencyStop, valvePositionChange, totalFlowChange, valveDeltaPchange` | `parent, GUI, fysical` |
| `virtualControl` | `statusCheck, execSequence, emergencyStop, valvePositionChange, totalFlowChange, valveDeltaPchange` | `GUI, fysical` |
| `fysicalControl` | `statusCheck, emergencyStop` | `fysical` |
| `maintenance` | `statusCheck` | (schema does NOT define `allowedSources.maintenance`; `isValidSourceForMode` returns `false` for every source &mdash; effectively monitoring-only) |
> [!WARNING]
> **Source contradiction:** `CONTRACT.md` describes `set.mode` as switching between "auto / manual control modes", but the schema defines four modes (`auto` / `virtualControl` / `fysicalControl` / `maintenance`) and `specificClass.setMode` validates against the schema's enum. The wider four-mode set is the implementation. TODO: tighten the prose in `CONTRACT.md` to enumerate the schema modes.
> [!WARNING]
> **Source contradiction:** the schema declares an `mode.allowedActions` table, but the running implementation only consults `isValidSourceForMode` &mdash; **`isValidActionForMode` is not implemented on VGC**. Action allow-lists are effectively dead config. TODO: either implement the action check (mirroring `rotatingMachine`'s pattern) or remove `allowedActions` from the schema.
---
## Data model &mdash; `getOutput()` shape
Composed each tick by `src/io/output.getOutput()`. Delta-compressed: consumers see only keys whose `getCurrentValue()` is non-null.
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
### Scalar keys
| Key | Type | Source | Notes |
|:---|:---|:---|:---|
| `mode` | string | `vgc.currentMode` | `auto` / `virtualControl` / `fysicalControl` / `maintenance`. |
| `maxDeltaP` | number | `vgc.maxDeltaP` | Cached max delta-P over registered valves (in output pressure unit, default `mbar`). Same data is also surfaced via the measurement-derived key `deltaMax_predicted_pressure`. |
### Measurement-derived keys
For every `(type, variant, position)` in MeasurementContainer with a finite value, the flattened output emits:
```
<position>_<variant>_<type>
```
| Example key | Unit | Source | Notes |
|:---|:---|:---|:---|
| `atEquipment_measured_flow` | m³/h | upstream source `flow.measured.*` events; `data.totalFlow` with `variant=measured` | Total measured flow at the group inlet. |
| `atEquipment_predicted_flow` | m³/h | written by `distributeFlow` as `sum(accepted)` | Sum of per-valve accepted flows after Kv-share + residual. |
| `deltaMax_predicted_pressure` | mbar | written by `calcMaxDeltaP` | Max `pressure.predicted.delta` across registered valves. |
> Delta compression: only changed fields are sent per tick. Consumers must cache and merge. See `outputUtils.formatMsg`.
<!-- END AUTOGEN -->
### Status badge
`io/output.getStatusBadge`:
```
<mode> | flow=<int> <flowUnit> | <N> valve(s) connected | (or 'No valves')
```
| State | Fill |
|:---|:---|
| `getAvailableValves().length > 0` | green dot |
| `getAvailableValves().length === 0` | red dot |
`flow` is the rounded `flow.measured.atEquipment`, or `flow.predicted.atEquipment` if no measured value is available.
---
## Configuration schema &mdash; editor form to config keys
Source of truth: `generalFunctions/src/configs/valveGroupControl.json` plus `nodeClass.buildDomainConfig` (which returns `{}` &mdash; no domain overrides).
### General (`config.general`)
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| Name | `general.name` | `"ValveGroupControl"` | Node label, status badge prefix (via `topic`). |
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
| Default unit | `general.unit` | `"unitless"` (schema) / `m3/h` (`configure()` overrides via `unitPolicy.output('flow')`) | Re-derived in `configure()`. |
| Enable logging | `general.logging.enabled` | `true` | Master switch. |
| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. |
### Functionality (`config.functionality`)
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| Position vs parent | `functionality.positionVsParent` | `""` (per `CONTRACT.md`) | Used in the Port-2 register payload sent to the upstream parent. (Not in the JSON schema; supplied at runtime from the editor.) |
| (hidden) | `functionality.softwareType` | `"valvegroupcontrol"` | Constant. |
| (hidden) | `functionality.role` | `"ValveGroupController"` | Constant. |
### Asset (`config.asset`)
VGC's asset block is informational &mdash; there is no curve to load, no model registry, no allowed-unit validation.
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. |
| Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | |
| Supplier | `asset.supplier` | `"Unknown"` | Informational. |
| Type | `asset.type` | `"valve"` | Classification only. |
| Sub-type | `asset.subType` | `"Unknown"` | |
| Model | `asset.model` | `"Unknown"` | Informational; no registry lookup. |
| Accuracy | `asset.accuracy` | `null` | |
### Mode (`config.mode`)
| Form field | Config key | Default | Range | Notes |
|:---|:---|:---|:---|:---|
| Mode | `mode.current` | `auto` | `auto` / `virtualControl` / `fysicalControl` / `maintenance` | The active operational mode. |
| (defaults) | `mode.allowedActions.<mode>` | see [Mode allow-lists](#mode--source-allow-lists) | enforced by `flowController` (NOT implemented &mdash; see warning above) |
| (defaults) | `mode.allowedSources.<mode>` | see [Mode allow-lists](#mode--source-allow-lists) | enforced by `isValidSourceForMode` |
### Sequences (`config.sequences`)
Per-sequence state-transition lists. Defaults:
| Sequence | States |
|:---|:---|
| `startup` | `[starting, warmingup, operational]` |
| `shutdown` | `[stopping, coolingdown, idle]` |
| `emergencystop` | `[emergencystop, off]` |
| `boot` | `[idle, starting, warmingup, operational]` |
`executeSequence(name)` iterates the list and awaits `state.transitionToState(stateName)` per step. The default state object is created at boot with `currentState = 'operational'` so `executeSequence` works without a pre-warmup phase. (See [Architecture &mdash; What VGC does NOT have](Reference-Architecture#what-vgc-does-not-have).)
### Calculation mode (`config.calculationMode`)
| Value | Description |
|:---|:---|
| `low` | Calculations run at fixed intervals (time-based). |
| `medium` (default) | Calculations run when new setpoints arrive or measured changes occur (event-driven). |
| `high` | Calculations run on all event-driven info, including every movement. |
> [!WARNING]
> `calculationMode` is in the schema but is not currently consulted by `specificClass` or `nodeClass`. The tick interval is fixed at `tickInterval = 1000 ms` and only retunable through `set.reconcileInterval`. TODO: wire `calculationMode` through or remove it.
### Flow reconciliation (runtime only)
`flowReconciliation` lives on the domain (not in the schema):
| Field | Default | Notes |
|:---|:---:|:---|
| `maxPasses` | `2` | Max iterations of the Kv-share residual loop. |
| `residualTolerance` | `0.001` | Stops loop when `|residual| < tolerance` (canonical units). |
These are read by `solveFlowDistribution` each call; not currently exposed via a topic or editor field.
### Unit policy
Source: `src/specificClass.js`.
| Quantity | Canonical (internal) | Output (rendered) | Required-unit |
|:---|:---|:---|:---:|
| Flow | `m3/s` | `m3/h` | ✓ |
| Pressure | `Pa` | `mbar` | ✓ |
`requireUnitForTypes: ['pressure', 'flow']` &mdash; MeasurementContainer rejects writes that omit `unit` for these types.
---
## Child registration
Source: `src/specificClass.js` `_registerValve` / `_registerSource` and `src/sources/fluidContract.js`.
| Software type | Filter | Wired to | Side-effect |
|:---|:---|:---|:---|
| `valve` | `child` exposes `updateFlow`, `state.getCurrentState`, `measurements` (`_isValveLike`) | Stored in `vgc.valves[id]`; events bound. | Subscribes to `state.emitter.positionChange` (&rarr; `calcValveFlows`) and `emitter.deltaPChange` (&rarr; `calcMaxDeltaP`). Triggers an initial `calcValveFlows` + `calcMaxDeltaP` + `refreshFluidContract`. |
| `machine` (incl. canonicalised `rotatingmachine`) | router callback | `registerSource` (`sources/fluidContract`) | Subscribes to 6 flow event names on `child.measurements.emitter`; subscribes to `child.emitter.fluidContractChange`. |
| `machinegroup` (incl. canonicalised `machinegroupcontrol`) | router callback | `registerSource` | Same as `machine`. |
| `pumpingstation` | router callback | `registerSource` | Same as `machine`. |
| `valvegroupcontrol` | router callback | `registerSource` | Cascaded VGC; accepted by router. Not exercised in production &mdash; see [Limitations](Reference-Limitations#cascaded-vgc-not-test-covered). |
Position labels accepted from children are `upstream`, `downstream`, `atEquipment` (and case variants &mdash; normalised internally).
### Source flow events
`bindSource` attaches a listener for every event name in `SOURCE_FLOW_EVENTS`:
```
flow.predicted.downstream
flow.predicted.atEquipment
flow.predicted.atequipment
flow.measured.downstream
flow.measured.atEquipment
flow.measured.atequipment
```
The handler reads `eventData.value` (number) and `eventData.unit` and writes `vgc.updateFlow(variant, value, 'atEquipment', unit)`. `variant` is derived from the event-name middle segment (`measured` vs `predicted`).
### Fluid contract reconciliation
See [Architecture &mdash; Source aggregation](Reference-Architecture#fluid-contract-aggregation) for the full reconciliation logic. The aggregated `fluidContract` is exposed via `vgc.getFluidContract()`:
```json
{
"status": "resolved" | "conflict" | "inferred" | "unknown",
"serviceType": "liquid" | "gas" | null,
"upstreamServiceTypes": ["liquid"],
"sourceCount": 2,
"message": "Upstream fluid resolved as liquid.",
"source": "valvegroupcontrol"
}
```
Changes are broadcast via `source.emitter.emit('fluidContractChange', ...)`.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, flow-distribution loop, source aggregation |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues and open questions |
| [EVOLV &mdash; Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
| [EVOLV &mdash; Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |