# Reference — 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. | 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). | ### Mode / source allow-lists A topic that survives the registry still passes through `flowController` → `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 — 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` — **`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 — `getOutput()` shape Composed each tick by `src/io/output.getOutput()`. Delta-compressed: consumers see only keys whose `getCurrentValue()` is non-null. ### 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: ``` __ ``` | 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`. ### Status badge `io/output.getStatusBadge`: ``` | flow= | 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 — editor form to config keys Source of truth: `generalFunctions/src/configs/valveGroupControl.json` plus `nodeClass.buildDomainConfig` (which returns `{}` — 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 — 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.` | see [Mode allow-lists](#mode--source-allow-lists) | enforced by `flowController` (NOT implemented — see warning above) | | (defaults) | `mode.allowedSources.` | 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 — 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']` — 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` (→ `calcValveFlows`) and `emitter.deltaPChange` (→ `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 — see [Limitations](Reference-Limitations#cascaded-vgc-not-test-covered). | Position labels accepted from children are `upstream`, `downstream`, `atEquipment` (and case variants — 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 — 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 — Architecture](Reference-Architecture) | Code map, flow-distribution loop, source aggregation | | [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | | [Reference — Limitations](Reference-Limitations) | Known issues and open questions | | [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules | | [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |