> Full topic contract, configuration schema, and child-registration filters for `valve`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/valve.json`.
>
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only. 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.
The pre-refactor topic `execSequence` carried `{source, action, parameter}` where `action` selected the verb. The command registry does not natively dispatch by payload content, so `execSequence` keeps its own descriptor whose handler forwards directly to the canonical `cmd.startup` / `cmd.shutdown` / `cmd.estop` handler based on `payload.action`. A deprecation warning fires once. Future-Phase-7 removal of `execSequence` is a behavioural change — callers must migrate to the canonical topics.
### Mode / source allow-lists
A topic that survives the registry still passes through `flowController.handleInput`:
```js
if (!this.isValidSourceForMode(source, this.host.currentMode)) {
this.logger.warn(`Source '${source}' is not valid for mode '${currentMode}'.`);
> `flowController.handleInput` currently enforces only the source side. The schema's `allowedActions` is defined but not gated in code — flagged as a TODO in [Architecture](Reference-Architecture#mode--source-allow-lists). Source: `nodes/valve/src/flow/flowController.js` line 13.
A rejected request logs at warn and short-circuits.
---
## Data model — `getOutput()` shape
Composed each tick by `src/io/output.js``buildOutput()`. Delta-compressed: consumers see only the keys that changed.
### Per-measurement keys
For every `(type, variant, position)` stored in MeasurementContainer with a finite value, the flattened output emits:
```
<position>_<variant>_<type>
```
This is the **legacy three-segment** key shape (no trailing `<childId>`). Position labels are normalised to lowercase. valve does **not** use the four-segment `<type>.<variant>.<position>.<childId>` shape that `rotatingMachine` emits.
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
| Key | Type | Unit | Sample / notes |
|:---|:---|:---|:---|
| `state` | string | — | `"operational"` — one of the FSM states. |
| `percentageOpen` | number | % | `60` — current position `0..100`. |
| `moveTimeleft` | number | s | `0` — seconds remaining on the current move. |
| `delta_predicted_pressure` | number | mbar | Predicted deltaP across the valve. Emitted on the next position tick once kv > 0 and a finite flow is known. |
| `downstream_predicted_flow` | number | m3/h | Last flow pushed via `data.flow` with `variant=predicted`. |
| `downstream_measured_flow` | number | m3/h | Last flow pushed via `data.flow` with `variant=measured`, or written by a registered flow measurement child. |
| `downstream_predicted_pressure` | number | mbar | Last predicted pressure written upstream. |
| `downstream_measured_pressure` | number | mbar | Last measured pressure from a registered pressure measurement child. |
(The `position` / `flow` / `deltaP` line only appears in `operational` / `warmingup` / `accelerating` / `decelerating`; other states show just `<mode>: <symbol>`.)
State symbols (per `STATE_SYMBOLS` map in `io/output.js`):
| State | Symbol | Fill |
|:---|:---:|:---|
| `off` | ⬛ | red |
| `idle` | ⏸️ | blue |
| `operational` | ⏵️ | green |
| `starting` | ⏯️ | yellow |
| `warmingup` | 🔄 | green |
| `accelerating` | ⏩ | yellow |
| `decelerating` | ⏪ | yellow |
| `stopping` | ⏹️ | yellow |
| `coolingdown` | ❄️ | yellow |
When `getFluidCompatibility().status` is `mismatch` or `conflict`, the badge is overridden to a yellow ring with `⚠ <message>` appended.
---
## Configuration schema — editor form to config keys
Source of truth: `generalFunctions/src/configs/valve.json` plus `nodeClass.buildDomainConfig`.
### General (`config.general`)
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| Name | `general.name` | `valve` | Re-derived in `configure()`. |
| Position vs parent | `functionality.positionVsParent` | `atEquipment` | One of `atEquipment` / `upstream` / `downstream`. Used in the child-register payload that goes UP to VGC. |
| Model | `asset.model` | `null` | Optional. If set, resolves curve + supplier / type / allowed units via `assetResolver.resolveAssetMetadata('valve', model)`. If null, the predictor uses the inline `valveCurve`. |
| Deployment unit | `asset.unit` | `null` | Must appear in the registry's allowed list for the model when set. |
| Service type | `serviceType` | derived (`gas` if not `liquid`) | Picks the hydraulic formula in `ValveHydraulicModel`. |
| Fluid density | `fluidDensity` | model default (997 / 1.204) | Sets `host.rho`. |
| Fluid temperature K | `fluidTemperatureK` | 293.15 | Sets `host.T`. |
| Gas choke ratio limit | `gasChokedRatioLimit` | 0.7 | Cap for the gas hydraulic formula. |
> [!WARNING]
> **Legacy asset fields rejected.** `supplier`, `category`, and `assetType` are no longer node config — the registry derives them from the model. Flows saved before the AssetResolver refactor will throw a startup error from `_rejectLegacyAssetFields` with a clear migration message. Re-open the node, re-select the model from the asset menu, and save.
### State times (`stateConfig.time`)
Set on the state machine via `nodeClass.buildDomainConfig` from editor fields:
| Form field | Config key | Notes |
|:---|:---|:---|
| Startup Time | `time.starting` | Time spent in `starting` before transitioning to `warmingup`. |
| Warmup Time | `time.warmingup` | Time in `warmingup` — **non-interruptible** safety. |
| Shutdown Time | `time.stopping` | Time in `stopping`. |
| Cooldown Time | `time.coolingdown` | Time in `coolingdown` — **non-interruptible** safety. |
> [!NOTE]
> TODO: confirm canonical defaults. `valve.json` does not declare them inline; they come from `generalFunctions/src/configs/state.json` or the parent state-machine schema. Source: `nodeClass.buildDomainConfig` lines 19–26.
### Movement (`stateConfig.movement`)
| Form field | Config key | Notes |
|:---|:---|:---|
| Reaction Speed | `movement.speed` | Position ramp rate (%/s). E.g. `1` means setpoint 60 from 0 takes ~60 s. |
### Sequences (`config.sequences`)
State-transition lists per sequence name. Defaults:
`requireUnitForTypes` means MeasurementContainer rejects writes that omit `unit` for these types. The hydraulic model itself reads back via fixed `FORMULA_UNITS = {pressure: 'mbar', flow: 'm3/h', temperature: 'K'}`.
### Calculation mode (`config.calculationMode`)
`low` / `medium` (default) / `high` — declared in the schema. TODO: confirm whether the dispatch path consults `calculationMode` for trigger frequency. Source: `valve.json` lines 346–366.
---
## Child registration
Source: `src/specificClass.js` lines 100–101 and `src/fluid/fluidCompatibility.js`.
valve **overrides**`BaseDomain.registerChild` so registrations fall into `FluidCompatibility.registerChild` rather than the generic ChildRouter. Upstream sources feed the fluid-contract aggregator; measurement children attach via the standard measurement handshake and land in `MeasurementRouter`.
| Software type | Side-effect | Subscribed events |
|:---|:---|:---|
| `machine` / `rotatingmachine` | Stored as upstream source; reads `getFluidContract()` or `asset.serviceType`, defaulting to `liquid` for the rotating-equipment family. Recomputes aggregate service type. | `fluidContractChange` |
| `measurement` (asset.type=`pressure`, position=`*`) | Routed through `MeasurementRouter.updatePressure(variant, value, position, unit)`; triggers a deltaP recompute. | `<type>.measured.<position>` |
| `measurement` (asset.type=`flow`, position=`*`) | Routed through `MeasurementRouter.updateFlow(variant, value, position, unit)`; triggers a deltaP recompute. | `<type>.measured.<position>` |
The valve's `_updateMeasurement` path (via `updateMeasurement(variant, subType, value, position, unit)`) currently handles `pressure` and `flow`. `power` is recognised but ignored.