Compare commits

...

9 Commits

Author SHA1 Message Date
znetsixe
74951e7a23 style: palette swatch → (domain-hue redesign 2026-05-21)
Sidebar swatch now follows function family rather than S88 level, so the
palette is visually identifiable instead of monochromatically blue. Editor-group
rectangles in flow.json still follow S88 — only the registerType color changed.
Full table + rationale: superproject .claude/rules/node-red-flow-layout.md §10.0
and .claude/refactor/OPEN_QUESTIONS.md (2026-05-21 entry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:05:53 +02:00
znetsixe
167b1026f1 docs(CONTRACT): document valve's lack of a maintenance state machine
valve's schema mode enum includes `maintenance` (which gates sources)
but the FSM has no `entermaintenance` / `exitmaintenance` states and
the schema's `sequences` block has only startup / shutdown /
emergencystop / boot. Maintenance mode therefore disables external
sources but doesn't run a maintenance sequence — different shape
from rotatingMachine. Added a Limitations section to the CONTRACT so
this is explicit rather than surfacing as a wiki TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:35:28 +02:00
znetsixe
7acd6c2ce0 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:37 +02:00
znetsixe
144460e6ba docs(wiki): regenerate topic-contract AUTOGEN block via wiki-gen
Replaces the agent-written placeholder inside Reference-Contracts.md with
the authoritative table generated from src/commands/index.js. Both the
BEGIN and END markers are normalized to the canonical form used by
`@evolv/wiki-gen`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:11:50 +02:00
znetsixe
87214788d2 docs(wiki): full 5-page wiki matching the rotatingMachine reference format
Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:42:11 +02:00
znetsixe
8c2b2c0f9c docs: add Folder & File Layout section per EVOLV convention
Each repo can now be read standalone for the file-naming convention. Full rule:
.claude/rules/node-architecture.md in the EVOLV superproject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:30:33 +02:00
znetsixe
68ebe4ebce feat(valve): resolve supplier+type from asset registry, reject legacy asset fields
Mirrors the rotatingMachine cutover: assetResolver derives supplier/type/
units from the model id; nodeClass throws a clear "re-select model and
save" error if the saved node still carries denormalized supplier/
category/assetType strings. valve.html defaults trimmed accordingly.

14/14 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:12:47 +02:00
znetsixe
95ccc77b25 docs(wiki): rewrite Home.md — correct FSM states + config keys for Section 9/10
Section 10 (State chart): replace invented opening/closing/closed states with
the real shared FSM states (accelerating/decelerating for moves; idle/starting/
warmingup/operational/stopping/coolingdown/emergencystop/off/maintenance for
lifecycle). Show all valid transitions from stateConfig.json allowedTransitions.
Document protected transitions (warmingup, coolingdown) and valve-specific
pre-shutdown ramp-to-zero behaviour.

Section 9 (Config): add missing editor fields from nodeClass.buildDomainConfig
(startup/warmup/shutdown/cooldown times, speed, serviceType, fluidDensity,
fluidTemperatureK, gasChokedRatioLimit). Correct config paths to match actual
stateConfig / runtimeOptions split.

Section 7 (Lifecycle): add FSM state labels to sequence diagram; show
accelerating → operational final step.

Sections 2/6/12/14: minor precision improvements (Port-2 note, abort-deadlock
recipe, execSequence Phase-7 removal warning).

Re-ran npm run wiki:all; AUTOGEN blocks intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:06:26 +02:00
znetsixe
43a17ad83f P11.6 wiki regen + Phase 10 private-test rewrites where applicable
For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic
contract) and 9 (data model) regenerated via npm run wiki:all. New
Unit column shows '<measure> (default <unit>)' for declared topics,
'—' otherwise. Effect column now uses descriptor.description (P11.2
field) overriding the generic per-prefix fallback.

For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files
moved off private nodeClass internals (_attachInputHandler, _commands,
_pendingExtras, _registerChild, _tick, etc.) to the public
BaseNodeAdapter surface (node.handlers.input, node.source.*).
+6 / +7 net new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:44:14 +02:00
12 changed files with 995 additions and 234 deletions

View File

@@ -21,3 +21,20 @@ Key points for this node:
- Stack same-level siblings vertically. - Stack same-level siblings vertically.
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right). - Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
- Wrap in a Node-RED group box coloured `#86bbdd` (Equipment Module). - Wrap in a Node-RED group box coloured `#86bbdd` (Equipment Module).
## Folder & File Layout
Every per-node file MUST use the folder name (`valve`) **exactly**, case-sensitive. Full rule: [`.claude/rules/node-architecture.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/node-architecture.md) in the EVOLV superproject.
| Path | Required name |
|---|---|
| Entry file | `valve.js` |
| Editor HTML | `valve.html` |
| Node adapter | `src/nodeClass.js` |
| Domain logic | `src/specificClass.js` |
| Editor JS modules | `src/editor/*.js` (extract when inline editor JS exceeds ~50 lines) |
| Tests | `test/{basic,integration,edge}/*.test.js` |
| Example flows | `examples/*.flow.json` |
When adding new files, read the rule above first to avoid drift.

View File

@@ -68,6 +68,18 @@ from the measurement container (e.g. `delta_predicted_pressure`,
(`idle → starting → warmingup → operational → accelerating → (`idle → starting → warmingup → operational → accelerating →
decelerating → stopping → coolingdown → idle`, plus `off`). decelerating → stopping → coolingdown → idle`, plus `off`).
## Limitations vs `rotatingMachine`
- **No `maintenance` state machine.** The schema's `mode.current` enum
accepts `maintenance` (gates sources via `isValidSourceForMode`), but
the FSM has no `entermaintenance` / `exitmaintenance` states and the
`sequences` schema declares only `startup`, `shutdown`, `emergencystop`,
and `boot`. Configuring `maintenance` mode therefore disables external
command sources but does not put the valve through a maintenance
sequence. Aligns with valve's role as a passive flow-controlled
actuator; lift to RM-style FSM if/when site maintenance procedures
require explicit state transitions.
## Events emitted by `source.measurements.emitter` ## Events emitted by `source.measurements.emitter`
The `MeasurementContainer` fires `<type>.<variant>.<position>` whenever The `MeasurementContainer` fires `<type>.<variant>.<position>` whenever

View File

@@ -11,7 +11,7 @@ module.exports = [
topic: 'set.mode', topic: 'set.mode',
aliases: ['setMode'], aliases: ['setMode'],
payloadSchema: { type: 'string' }, payloadSchema: { type: 'string' },
description: 'Switch the valve between auto / manual control modes.', description: 'Switch the operating mode. Allowed: `auto`, `virtualControl`, `fysicalControl`, `maintenance` (schema-validated in `valve.json` → `mode.current`).',
handler: handlers.setMode, handler: handlers.setMode,
}, },
{ {

View File

@@ -11,6 +11,8 @@ class nodeClass extends BaseNodeAdapter {
static statusInterval = 1000; static statusInterval = 1000;
buildDomainConfig(uiConfig) { buildDomainConfig(uiConfig) {
_rejectLegacyAssetFields(uiConfig);
const flowUnit = _resolveUnit(uiConfig.unit, 'volumeFlowRate', 'm3/h'); const flowUnit = _resolveUnit(uiConfig.unit, 'volumeFlowRate', 'm3/h');
const asNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : undefined; }; const asNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : undefined; };
Valve._pendingExtras = { Valve._pendingExtras = {
@@ -29,7 +31,25 @@ class nodeClass extends BaseNodeAdapter {
gasChokedRatioLimit: asNum(uiConfig.gasChokedRatioLimit), gasChokedRatioLimit: asNum(uiConfig.gasChokedRatioLimit),
}, },
}; };
return { general: { unit: flowUnit }, asset: { unit: flowUnit } }; return {
general: { unit: flowUnit },
asset: { model: uiConfig.model || null, unit: flowUnit },
};
}
}
// See rotatingMachine/src/nodeClass.js for the rationale. Same cutover rule.
function _rejectLegacyAssetFields(uiConfig) {
const offenders = ['supplier', 'category', 'assetType'].filter((k) => {
const v = uiConfig[k];
return typeof v === 'string' && v.trim() !== '';
});
if (offenders.length > 0) {
throw new Error(
`valve: legacy asset field(s) [${offenders.join(', ')}] are saved on this node. ` +
`After the AssetResolver refactor these are derived from the model id. ` +
`Open the node in the editor, re-select the model, and save to migrate.`,
);
} }
} }

View File

@@ -5,7 +5,7 @@
// the logic; this file wires them together and preserves the public surface // the logic; this file wires them together and preserves the public surface
// the test suite + parents (VGC, MGC, pumpingStation) depend on. // the test suite + parents (VGC, MGC, pumpingStation) depend on.
const { BaseDomain, UnitPolicy, state } = require('generalFunctions'); const { BaseDomain, UnitPolicy, state, assetResolver } = require('generalFunctions');
const { ValveHydraulicModel, normalizeServiceType } = require('./hydraulicModel'); const { ValveHydraulicModel, normalizeServiceType } = require('./hydraulicModel');
const { FluidCompatibility, normalizeOptional } = require('./fluid/fluidCompatibility'); const { FluidCompatibility, normalizeOptional } = require('./fluid/fluidCompatibility');
const { SupplierCurvePredictor } = require('./curve/supplierCurve'); const { SupplierCurvePredictor } = require('./curve/supplierCurve');
@@ -67,6 +67,12 @@ class Valve extends BaseDomain {
}); });
this.model = this.config.asset?.model; this.model = this.config.asset?.model;
// Derived asset metadata (supplier, type, allowed units) — null if the
// model isn't in the registry. Valve tolerates a null model + inline
// configCurve, so we don't hard-fail here; the curve predictor logs.
this.assetMetadata = this.model
? assetResolver.resolveAssetMetadata('valve', this.model)
: null;
this.curvePredictor = new SupplierCurvePredictor({ this.curvePredictor = new SupplierCurvePredictor({
logger: this.logger, logger: this.logger,
model: this.model, model: this.model,

View File

@@ -14,7 +14,7 @@
<script> <script>
RED.nodes.registerType("valve", { RED.nodes.registerType("valve", {
category: "EVOLV", category: "EVOLV",
color: "#86bbdd", // color for the node based on the S88 schema color: "#3CAEA3",
defaults: { defaults: {
// Define default properties // Define default properties
@@ -25,11 +25,11 @@
processOutputFormat: { value: "process" }, processOutputFormat: { value: "process" },
dbaseOutputFormat: { value: "influxdb" }, dbaseOutputFormat: { value: "influxdb" },
//define asset properties // Asset identifier surface. supplier/category/assetType are derived
// at runtime via assetResolver.resolveAssetMetadata(model). Do NOT
// add them back here. See generalFunctions/src/registry/README.md.
uuid: { value: "" }, uuid: { value: "" },
supplier: { value: "" }, assetTagNumber: { value: "" },
category: { value: "" },
assetType: { value: "" },
model: { value: "" }, model: { value: "" },
unit: { value: "" }, unit: { value: "" },
@@ -54,7 +54,10 @@
icon: "font-awesome/fa-toggle-on", icon: "font-awesome/fa-toggle-on",
label: function () { label: function () {
return (this.positionIcon || "") + " " + (this.category ? this.category.slice(0, -1) : "Valve"); // No more `this.category` on the node — derive from the model if needed,
// else fall back to a generic name.
const stem = this.model ? this.model : "Valve";
return (this.positionIcon || "") + " " + stem;
}, },
oneditprepare: function() { oneditprepare: function() {
@@ -128,6 +131,7 @@
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label> <label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
<select id="node-input-dbaseOutputFormat" style="width:60%;"> <select id="node-input-dbaseOutputFormat" style="width:60%;">
<option value="influxdb">influxdb</option> <option value="influxdb">influxdb</option>
<option value="frost">frost</option>
<option value="json">json</option> <option value="json">json</option>
<option value="csv">csv</option> <option value="csv">csv</option>
</select> </select>

View File

@@ -1,259 +1,134 @@
# valve # valve
> **Reflects code as of `e27135b` · regenerated `2026-05-11` via `npm run wiki:all`** ![code-ref](https://img.shields.io/badge/code--ref-8c2b2c0-blue) ![s88](https://img.shields.io/badge/S88-Equipment_Module-86bbdd) ![status](https://img.shields.io/badge/status-pending--review-orange)
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
## 1. What this node is A `valve` models a single actuated throttling valve. It loads a supplier Kv-vs-position characteristic curve, drives a position FSM (`accelerating` / `decelerating` through `operational`), and recomputes pressure drop from flow + Kv via a hydraulic model that picks a liquid or gas formula by `serviceType`. Used standalone, or as a child of `valveGroupControl`, downstream of a `rotatingMachine` / `machineGroupControl` / `pumpingStation`.
**valve** models a single throttling valve. It loads a supplier characteristic curve (Kv-vs-position), drives an FSM-style move sequence for opening/closing, and recomputes pressure drop across the valve from current flow + Kv. Used standalone or as a child of `valveGroupControl` when grouped. > [!NOTE]
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
## 2. Position in the platform ---
## At a glance
| Thing | Value |
|:---|:---|
| What it represents | One actuated throttling valve &mdash; supplier Kv curve, position FSM, deltaP estimate |
| S88 level | Equipment Module |
| Use it when | You need a position-controlled valve whose deltaP depends on flow, Kv(position), and service-type (gas / liquid) |
| Don't use it for | Fixed-restriction orifices, non-return / check valves, or curveless throttling devices (no fallback model) |
| Children it accepts | Upstream sources (`rotatingmachine`, `machinegroup` / `machinegroupcontrol`, `pumpingstation`, `valvegroupcontrol`) for fluid-contract tracking; `measurement` for pressure / flow |
| Parents it talks to | `valveGroupControl` (typical) or any node that issues `set.position` / `cmd.startup` / `cmd.shutdown` |
---
## How it fits
```mermaid ```mermaid
flowchart LR flowchart LR
vgc[valveGroupControl<br/>Unit]:::unit -->|set.position| this[valve<br/>Equipment]:::equip parent[valveGroupControl]:::unit -->|set.position<br/>cmd.startup / shutdown| v[valve<br/>Equipment]:::equip
src[machine / MGC / PS<br/>upstream source]:::unit -->|child.register| this src["rotatingMachine /<br/>MGC / pumpingStation"]:::unit -->|child.register<br/>(fluid contract)| v
meas[measurement<br/>type=pressure / flow]:::ctrl -.data.-> this m_p[measurement<br/>pressure]:::ctrl -.measured.-> v
this -->|child.register| vgc m_f[measurement<br/>flow]:::ctrl -.measured.-> v
this -->|evt.deltaPChange| vgc v -->|child.register| parent
v -.->|evt.deltaPChange<br/>evt.fluidCompatibilityChange<br/>evt.fluidContractChange| parent
classDef unit fill:#50a8d9,color:#000 classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000 classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000 classDef ctrl fill:#a9daee,color:#000
``` ```
S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`. S88 colours are anchored in `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix ---
| Capability | Status | Notes | ## Try it &mdash; 3-minute demo
|---|---|---|
| Predicts deltaP from flow + Kv | ✅ | Hydraulic model picks liquid vs gas formula per `serviceType`. |
| Loads supplier curve by model name | ✅ | `asset.model` resolved through `loadModel`; inline curve override supported. |
| Position move FSM | ✅ | `opening` / `closing` states with interruptible setpoints. |
| Startup / shutdown sequences | ✅ | Pre-shutdown ramps to position 0 when operational. |
| Emergency-stop sequence | ✅ | Aliased `cmd.estop` → state-machine `emergencystop`. |
| Fluid-contract aggregation | ✅ | Tracks upstream service type via registered sources. |
| Gas-choke detection | ⚠️ | Capped at `gasChokedRatioLimit`; surfaced in `hydraulicDiagnostics`. |
| Multi-parent registration | ⚠️ | Allowed but not exercised in production tests. |
## 4. Code map Import the basic example flow, deploy, and drive a single valve through a position move.
```mermaid ```bash
flowchart TB curl -X POST -H 'Content-Type: application/json' \
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] --data @nodes/valve/examples/basic.flow.json \
nc["buildDomainConfig()<br/>static DomainClass, commands"] http://localhost:1880/flow
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Valve.configure()<br/>wires concern modules<br/>installs FluidCompatibility registerChild"]
end
subgraph concerns["src/ concern modules"]
state["state/<br/>stateBindings → positionChange"]
fluid["fluid/<br/>FluidCompatibility"]
curve["curve/<br/>SupplierCurvePredictor"]
meas["measurement/<br/>MeasurementRouter + FORMULA_UNITS"]
flow["flow/<br/>FlowController (setpoint, sequences)"]
io["io/<br/>buildOutput + buildStatusBadge"]
hyd["hydraulicModel.js<br/>ValveHydraulicModel"]
end
nc --> sc
sc --> state
sc --> fluid
sc --> curve
sc --> meas
sc --> flow
sc --> io
sc --> hyd
``` ```
| Module | Owns | Read first if you're changing… | > [!NOTE]
|---|---|---| > The shipped `examples/{basic,integration,edge}.flow.json` files are minimal stubs (one inject &rarr; valve &rarr; debug). A tiered `01 - Basic Manual Control.json` / `02 - Integration with Valve Group.json` / `03 - Dashboard Visualization.json` set, matching the `rotatingMachine` template, is on the backlog. Until then, drive the node directly with injects.
| `state/` | Bindings from state-machine `positionChange``updatePosition()` | Move-finished triggers. |
| `fluid/` | Service-type compatibility, contract aggregation | Gas-vs-liquid mismatch warnings. |
| `curve/` | Supplier Kv curve load + interpolation | Curve fitting, model selection. |
| `measurement/` | Pressure/flow routing + deltaP recompute | What triggers a recalc. |
| `flow/` | Sequence + setpoint execution | Startup / shutdown / move semantics. |
| `io/` | Port-0 output shape + status badge | What lands on the wire each tick. |
## 5. Topic contract What to send after deploy (the topics map one-to-one to entries in [Reference &mdash; Contracts](Reference-Contracts#topic-contract)):
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. 1. `set.mode = virtualControl` &mdash; lets the GUI source drive the valve (parent path is for grouped use).
2. `cmd.startup` &mdash; FSM runs `idle &rarr; starting &rarr; warmingup &rarr; operational`.
3. `set.position = {setpoint: 60}` (position %) &mdash; valve ramps from 0 to 60; state goes `operational &rarr; accelerating &rarr; operational`. Each position tick fires a Kv lookup + deltaP recompute.
4. `data.flow = {variant: 'measured', value: 25, position: 'downstream', unit: 'm3/h'}` &mdash; push flow so the hydraulic model has something to chew on. `delta_predicted_pressure` updates and `evt.deltaPChange` fires upward.
5. `cmd.shutdown` &mdash; if currently `operational`, the controller first ramps to position 0, then transitions `stopping &rarr; coolingdown &rarr; idle`.
<!-- BEGIN AUTOGEN: topic-contract --> > [!IMPORTANT]
> **GIF needed.** Demo recording of steps 1&ndash;5 with the live status badge. Save as `wiki/_partial-gifs/valve/01-basic-demo.gif`, target &le; 1&nbsp;MB after `gifsicle -O3 --lossy=80`.
| Canonical topic | Aliases | Payload | Effect | ---
|---|---|---|---|
| `set.mode` | `setMode` | `string` | Replaces the named state value with the supplied payload. |
| `cmd.startup` | _(none)_ | `any` | Triggers an action / sequence — not idempotent. |
| `cmd.shutdown` | _(none)_ | `any` | Triggers an action / sequence — not idempotent. |
| `cmd.estop` | `emergencystop`, `emergencyStop` | `any` | Triggers an action / sequence — not idempotent. |
| `execSequence` | _(none)_ | `object` | _(see handler)_ |
| `set.position` | `execMovement` | `object` | Replaces the named state value with the supplied payload. |
| `data.flow` | `updateFlow` | `object` | Pushes a value into the node's measurement stream. |
| `query.curve` | `showcurve` | `any` | Read-only query; node replies on the same msg. |
| `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. |
<!-- END AUTOGEN: topic-contract --> ## The seven things you'll send
## 6. Child registration | Topic | Aliases | Payload | What it does |
|:---|:---|:---|:---|
| `set.mode` | `setMode` | `"auto"` \| `"virtualControl"` \| `"fysicalControl"` \| `"maintenance"` | Switch operational mode. Source allow-list per mode (defaults from `valve.json`). |
| `cmd.startup` | &mdash; | `{ source?: string }` | Run the configured `startup` sequence (default `[starting, warmingup, operational]`). |
| `cmd.shutdown` | &mdash; | `{ source?: string }` | Run `shutdown`. If currently `operational`, first ramps the valve to position 0, then transitions `stopping &rarr; coolingdown &rarr; idle`. |
| `cmd.estop` | `emergencystop`, `emergencyStop` | `{ source?: string, action?: string }` | Trigger an emergency stop &mdash; runs the `emergencystop` sequence (default `[emergencystop, off]`). |
| `set.position` | `execMovement` | `{ source?: string, action?: string, setpoint: number }` | Move the valve to a position (control-%, `0..100`). Setpoint is coerced to `Number`. |
| `data.flow` | `updateFlow` | `{ variant, value, position, unit? }` &mdash; `variant ∈ {'measured','predicted'}` | Push a flow measurement; triggers a Kv lookup + deltaP recompute via the hydraulic model. |
| `query.curve` | `showcurve` | any | Reply on Port 0 with `{ topic: 'Showing curve', payload: <curve snapshot> }`. |
valve overrides BaseDomain's default `registerChild` with `FluidCompatibility.registerChild` so upstream-source contracts feed the fluid aggregator. Measurement children attach through the generic measurement handshake. Plus the registration topic emitted upward at startup and accepted from real `measurement` children:
```mermaid | Topic | Aliases | Payload |
flowchart LR |:---|:---|:---|
subgraph kids["accepted children (softwareType)"] | `child.register` | `registerChild` | child Node-RED id (string); `msg.positionVsParent` carries the position label |
src["machine / rotatingmachine /<br/>machinegroup / pumpingstation /<br/>valvegroupcontrol"]:::unit
m["measurement"]:::ctrl The legacy umbrella `execSequence` (`{action: 'startup' \| 'shutdown' \| 'emergencystop'}`) is still accepted &mdash; it forwards to the canonical `cmd.*` handler and logs a one-time deprecation warning. Scheduled for removal in Phase 7.
end
src -->|getFluidContract| fluid[FluidCompatibility<br/>aggregates serviceType] ---
m -->|"&lt;type&gt;.measured.&lt;position&gt;"| router[MeasurementRouter<br/>updatePressure / updateFlow]
router --> deltaP[updateDeltaP<br/>writes pressure.predicted.delta] ## What you'll see come out
fluid --> evt1[evt.fluidCompatibilityChange]
deltaP --> evt2[evt.deltaPChange] Sample Port 0 message (delta-compressed, while operational at ~60 % open with a 25 m³/h flow):
classDef unit fill:#50a8d9,color:#000
classDef ctrl fill:#a9daee,color:#000 ```json
{
"topic": "valve#valve_a",
"payload": {
"state": "operational",
"percentageOpen": 60,
"moveTimeleft": 0,
"mode": "auto",
"downstream_measured_flow": 25,
"downstream_predicted_flow": 0,
"delta_predicted_pressure": 84
}
}
``` ```
| softwareType | onRegister side-effect | Subscribed events | Key shape: **`<position>_<variant>_<type>`** &mdash; the legacy three-segment shape. Position labels are lowercase (`downstream`, `delta`, `upstream`). `valve` does **not** use the four-segment `<type>.<variant>.<position>.<childId>` shape that `rotatingMachine` emits.
|---|---|---|
| `machine` / `rotatingmachine` | Stored as upstream source; reads `getFluidContract()` or default `liquid`. | `fluidContractChange`. |
| `machinegroup` / `machinegroupcontrol` | Same; recomputes aggregate service type. | `fluidContractChange`. |
| `pumpingstation` | Same. | `fluidContractChange`. |
| `valvegroupcontrol` | Same. | `fluidContractChange`. |
| `measurement` | Routed via measurement handshake; values land in MeasurementContainer. | `<type>.measured.<position>`. |
## 7. Lifecycle — what one event does | Field | Meaning |
|:---|:---|
| `state` | Current FSM state. See [Architecture &mdash; FSM](Reference-Architecture#fsm). |
| `percentageOpen` | Current position (`0..100`). 0 = closed, 100 = fully open. |
| `moveTimeleft` | Seconds remaining on the current position move (0 when stationary). |
| `mode` | One of `auto` / `virtualControl` / `fysicalControl` / `maintenance`. |
| `delta_predicted_pressure` | Predicted deltaP across the valve (output unit `mbar`). |
| `downstream_predicted_flow` / `_measured_flow` | Last flow pushed via `data.flow` (output unit `m3/h`). |
| `downstream_measured_pressure` / `_predicted_pressure` | Pressure measurements pushed via the `MeasurementRouter`. |
```mermaid ---
sequenceDiagram
participant parent as valveGroupControl
participant valve as valve
participant state as state FSM
participant hyd as hydraulicModel
participant out as Port-0
parent->>valve: set.position { setpoint: 60 } ## Need more?
valve->>state: moveTo(60)
state-->>valve: positionChange ticks
valve->>valve: predictKv(position)
valve->>hyd: calculateDeltaPMbar(q, kv, downP, rho, T)
hyd-->>valve: { deltaPMbar, details }
valve->>valve: write pressure.predicted.delta
valve->>parent: emitter.emit('deltaPChange', deltaP)
valve->>out: msg{topic, payload (delta-compressed)}
```
## 8. Data model — `getOutput()` | Page | What you'll find |
|:---|:---|
| [Reference &mdash; Contracts](Reference-Contracts) | Full topic contract, config schema, child registration filters |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, FSM, hydraulic model, lifecycle |
| [Reference &mdash; Examples](Reference-Examples) | Shipped example flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | When not to use, known limitations, open questions |
What lands on Port 0. Composed in `io/output.buildOutput`, then delta-compressed by `outputUtils.formatMsg`. [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) &middot; [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) &middot; [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
<!-- BEGIN AUTOGEN: data-model -->
| Key | Type | Unit | Sample |
|---|---|---|---|
| `state` | string | — | `"operational"` |
| `percentageOpen` | number | % | `0` |
| `moveTimeleft` | number | s | `0` |
| `mode` | string | — | `"auto"` |
| `downstream_predicted_flow` | number | m3/h | `0` |
| `downstream_measured_flow` | number | m3/h | _(emitted when measurement child present)_ |
| `downstream_predicted_pressure` | number | mbar | _(emitted when upstream pressure present)_ |
| `downstream_measured_pressure` | number | mbar | _(emitted when measurement child present)_ |
| `delta_predicted_pressure` | number | mbar | `0` |
<!-- END AUTOGEN: data-model -->
Measurement-derived keys follow the legacy `<position>_<variant>_<type>` shape (e.g. `downstream_predicted_flow`, `delta_predicted_pressure`) and are emitted only when the container holds a finite value.
## 9. Configuration — editor form ↔ config keys
```mermaid
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Mode]
f2[Asset model]
f3[Service type]
f4[Diameter]
f5[Fluid density / temperature]
f6[Inline valveCurve override]
end
subgraph config["Domain config slice"]
c1[mode.current]
c2[asset.model]
c3[asset.serviceType]
c4[asset.valveDiameter]
c5[asset.fluidDensity / fluidTemperatureK]
c6[asset.valveCurve]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
```
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Mode | `mode.current` | per schema | enum | `setMode`, `flowController` |
| Asset model | `asset.model` | `null` | string | `SupplierCurvePredictor` |
| Service type | `asset.serviceType` | per asset | `gas` / `liquid` | `ValveHydraulicModel` |
| Diameter | `asset.valveDiameter` | per asset | > 0 (m) | curve key selection |
| Fluid density | `asset.fluidDensity` | model default | > 0 (kg/m³) | hydraulic formula |
| Fluid temperature | `asset.fluidTemperatureK` | model default | > 0 (K) | hydraulic formula |
| Choked-flow cap | `asset.gasChokedRatioLimit` | per asset | 01 | gas formula clamp |
## 10. State chart
```mermaid
stateDiagram-v2
[*] --> off
off --> idle: cmd.startup
idle --> opening: set.position > 0
opening --> operational: position reached
operational --> opening: set.position changed
operational --> closing: set.position < current
closing --> closed: position == 0
closed --> opening: set.position > 0
operational --> stopping: cmd.shutdown (ramps to 0)
stopping --> idle: cooldown elapsed
operational --> emergencystop: cmd.estop
emergencystop --> off: cmd.reset
```
The `opening` / `closing` states cover the move-in-progress window; `positionChange` ticks fire until the setpoint is reached, then the FSM lands on `operational`. Pre-shutdown ramp to 0 is enforced by `FlowController.executeSequence('shutdown')`.
## 11. Examples
| Tier | File | What it shows | Mandatory? |
|---|---|---|---|
| Basic | `examples/01-Basic.flow.json` | Inject `set.position` + dashboard, no parent | ✅ |
| Integration | `examples/02-Integration.flow.json` | valve + VGC + upstream source | ✅ |
| Dashboard | `examples/03-Dashboard.flow.json` | Live FlowFuse charts (position, ΔP, flow) | ⭕ |
Screenshots under `wiki/_partial-screenshots/valve/` when produced. Docker compose snippet under `examples/README.md`.
## 12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| Status badge shows `⚠ no input` | Did any pressure / flow measurement register? Watch Port 2. | Editor debug tap on Port 2 |
| `delta_predicted_pressure` stuck at zero | Is `kv > 0`? FSM may be in `off` / `closed`. | `state.getCurrentState()` |
| Gas mismatch warning on status badge | `fluidCompatibility.status` is `mismatch` / `conflict`. | `getFluidCompatibility()` |
| `query.curve` returns empty curve | Asset model not found by `loadModel`; fallback to `config.asset.valveCurve`. | `SupplierCurvePredictor.snapshot()` |
| deltaP non-finite | Downstream gauge pressure absolute term ≤ 0, or choked ratio reached. | `hydraulicDiagnostics` |
> 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 for a **throttling element** with a known Kv curve. For a fixed-restriction orifice with no actuator, model the deltaP externally.
- Don't use valve to model a non-return / check valve — no position control or curve fitting is exposed.
- Skip valve when an upstream source provides flow directly and no pressure-drop estimate is needed; just wire the source straight to the parent.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | Gas-choke detection is a hard cap, not a smooth transition — chart traces show a step at the choked-ratio limit. | `hydraulicModel.js` |
| 2 | Multi-parent registration is allowed but not exercised in production tests. | CONTRACT.md `## Children registered by this node` |
| 3 | `set.position` move sequences are interruptible but tests cover happy-path only. | P10 test-suite refactor |

View File

@@ -0,0 +1,300 @@
# Reference &mdash; Architecture
![code-ref](https://img.shields.io/badge/code--ref-8c2b2c0-blue)
> [!NOTE]
> Code structure for `valve`: the three-tier sandwich, the `src/` layout, the position FSM, the hydraulic-model pipeline, the lifecycle, and the output ports. For an intuitive overview, return to [Home](Home).
>
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
---
## Three-tier code layout
```
nodes/valve/
|
+-- valve.js entry: RED.nodes.registerType('valve', NodeClass)
|
+-- src/
| nodeClass.js extends BaseNodeAdapter (Node-RED bridge)
| specificClass.js extends BaseDomain (orchestration only)
| hydraulicModel.js ValveHydraulicModel + normalizeServiceType
| |
| +-- commands/
| | index.js topic descriptors
| | handlers.js pure handler functions
| |
| +-- curve/
| | supplierCurve.js SupplierCurvePredictor (Kv-vs-position load + interp)
| |
| +-- fluid/
| | fluidCompatibility.js FluidCompatibility — upstream service-type aggregation
| |
| +-- measurement/
| | measurementRouter.js MeasurementRouter + FORMULA_UNITS
| |
| +-- flow/
| | flowController.js handleInput, executeSequence, setpoint (pre-shutdown ramp)
| |
| +-- state/
| | stateBindings.js wires state.emitter('positionChange') → updatePosition()
| |
| +-- io/
| output.js buildOutput + buildStatusBadge
```
### Tier responsibilities
| Tier | File | What it owns | Touches `RED.*` |
|:---|:---|:---|:---:|
| entry | `valve.js` | Type registration | Yes |
| nodeClass | `src/nodeClass.js` | UI-config &rarr; domain config, legacy-asset-field reject, status-badge polling (`statusInterval=1000`). No tick loop (`tickInterval=null`) &mdash; event-driven. | Yes |
| specificClass | `src/specificClass.js` | Wire concern modules in `configure()`; expose the public surface tests + parents (VGC) depend on (`handleInput`, `setMode`, `updatePosition`, `updateFlow`, `updatePressure`, `registerChild`, `showCurve`, `getOutput`, &hellip;). Overrides `BaseDomain.registerChild` so upstream-source registration falls into `FluidCompatibility` instead of the generic ChildRouter. | No |
`specificClass` is stitching. All real work lives in the concern modules: position bindings in `state/`, deltaP math in `hydraulicModel.js`, Kv interpolation in `curve/`, measurement &rarr; deltaP plumbing in `measurement/`, mode + sequences in `flow/`, fluid contract aggregation in `fluid/`, and Port-0 shaping in `io/`.
---
## FSM
> [!NOTE]
> The state machine is declared in `generalFunctions/src/state/stateConfig.json`. Same allowed-transition graph as `rotatingMachine`, with `accelerating` / `decelerating` reused for position moves up / down.
```mermaid
stateDiagram-v2
[*] --> idle
idle --> starting: cmd.startup
idle --> off
idle --> maintenance
starting --> warmingup: timer (time.starting)
warmingup --> operational: timer (time.warmingup) [protected]
operational --> accelerating: set.position up
operational --> decelerating: set.position down
operational --> stopping: cmd.shutdown
accelerating --> operational: target reached
decelerating --> operational: target reached
stopping --> coolingdown: timer (time.stopping)
coolingdown --> idle: timer (time.coolingdown) [protected]
coolingdown --> off
off --> idle: boot
off --> maintenance
maintenance --> off
maintenance --> idle
note right of operational
any state -> emergencystop via cmd.estop
from emergencystop: idle / off / maintenance
end note
```
Default sequences (from `valve.json`):
| Sequence | States |
|:---|:---|
| `startup` | `[starting, warmingup, operational]` |
| `shutdown` | `[stopping, coolingdown, idle]` |
| `emergencystop` | `[emergencystop, off]` |
| `boot` | `[idle, starting, warmingup, operational]` |
### Pre-shutdown ramp to zero
`FlowController.executeSequence('shutdown')` checks the FSM. When the valve is `operational` it first calls `setpoint(0)` &mdash; the position-ramp to fully closed is interruptible &mdash; then iterates the sequence states.
### Protected states
`warmingup` and `coolingdown` are **protected** at the state-machine layer (same mechanism as `rotatingMachine`). Aborts during these phases are ignored to preserve safety guarantees.
> [!NOTE]
> Whether `valve` adopts the `sequenceAbortToken` mechanism from `rotatingMachine` (2026-05-15) for mid-shutdown re-engage races is an open question. TODO: confirm from `generalFunctions/src/state/state.js` whether valve inherits the token automatically. Source: `nodes/valve/src/flow/flowController.js`.
### Position-move bindings
`src/state/stateBindings.js` wires the underlying state machine's `positionChange` event to `host.updatePosition()`. Every position tick triggers:
1. `host.kv = host.curvePredictor.predictKvForPosition(x)` &mdash; Kv lookup against the supplier curve.
2. `MeasurementRouter.updateDeltaP(currentFlow, kv, downstreamP)` &mdash; recompute the hydraulic deltaP and write `pressure.predicted.delta`.
3. `host.emitter.emit('deltaPChange', deltaP)` &mdash; upward to the parent VGC.
`updatePosition()` is a no-op outside of `operational` / `accelerating` / `decelerating` (see `MeasurementRouter.updatePositionDependent`).
---
## Hydraulic + measurement pipeline
```mermaid
flowchart TB
set[set.position]:::input --> fc[FlowController.setpoint]
fc --> moveTo[state.moveTo]
moveTo --> tick[state.emitter 'positionChange']
tick --> upd[updatePosition]
upd --> kv[curvePredictor.predictKvForPosition]
fdat[data.flow]:::input --> mr[MeasurementRouter.updateFlow]
fpres[measurement child<br/>pressure.measured.*]:::input --> mp[MeasurementRouter.updatePressure]
mr --> dp[updateDeltaP]
mp --> dp
kv --> dp
dp --> hyd[ValveHydraulicModel<br/>calculateDeltaPMbar]
hyd --> write[write pressure.predicted.delta]
write --> emit[emitter 'deltaPChange']
write --> out[Port 0]
classDef input fill:#a9daee,color:#000
```
### Curve loading
At `configure()` startup:
1. `assetResolver.resolveAssetMetadata('valve', model)` resolves supplier / type / allowed units from `generalFunctions/datasets/assetData/` &mdash; **may return null** for valve; the predictor tolerates an inline `asset.valveCurve` fallback.
2. `SupplierCurvePredictor` is constructed with the model, the inline curve override, density, temperature, and valve diameter.
3. `predictKv` (the curve-evaluation function) is exposed on the host; `host.curveSelection` records which `(densityKey, diameterKey)` lane of the dataset is in use.
The `asset.valveCurve` schema is a nested map keyed by gas-density (kg per nm³) and valve diameter (mm); the leaf carries `{x: [position%], y: [Kv (m³/h)]}` lookup tables.
### Hydraulic formula selection
`ValveHydraulicModel.calculateDeltaPMbar` picks one of two formulas by `serviceType`:
| serviceType | Formula | Notes |
|:---|:---|:---|
| `liquid` | `deltaP_bar = (Q / Kv)^2 * (rho / 1000)` | Density override via `runtimeOptions.fluidDensity` (default 997 kg/m³). |
| `gas` | `deltaP_bar = (Q^2 * rho * T) / (514^2 * Kv^2 * P2_abs)` | Density (default 1.204), absolute downstream pressure, temperature K. Capped at `gasChokedRatioLimit * P2_abs` when choked. |
Inputs are validated: `kv > 0`, `flow !== 0`, and (for gas) a finite downstream gauge pressure are required &mdash; otherwise the function returns `null` and the router skips the write.
### Formula units are pinned
`measurement/measurementRouter.js` declares:
```js
const FORMULA_UNITS = Object.freeze({ pressure: 'mbar', flow: 'm3/h', temperature: 'K' });
```
The hydraulic model expects q in m³/h, downstream gauge in mbar, and T in K. The router reads MeasurementContainer values back in these units before calling `calculateDeltaPMbar` regardless of the per-node `unitPolicy.output.*` rendering choices.
### Unit policy
Source: `src/specificClass.js` lines 20&ndash;24.
| Quantity | Canonical (internal) | Output (rendered) | Required-unit |
|:---|:---|:---|:---:|
| Pressure | `Pa` | `mbar` | ✓ |
| Flow | `m3/s` | `m3/h` | ✓ |
| Temperature | `K` | `C` | ✓ |
`requireUnitForTypes` means MeasurementContainer rejects writes that omit `unit` for these types.
---
## Lifecycle &mdash; what one event does
```mermaid
sequenceDiagram
autonumber
participant parent as VGC / GUI
participant v as valve
participant fc as flowController
participant fsm as state (FSM)
participant hyd as hydraulicModel
participant out as Port 0 / parent
parent->>v: set.position {setpoint: 60}
v->>fc: flowController.handleInput('parent','execMovement', 60)
fc->>fc: isValidSourceForMode check
fc->>fsm: setpoint(60) → state.moveTo(60)
fsm-->>v: positionChange events per move tick
v->>v: kv = curvePredictor.predictKvForPosition(pos)
v->>hyd: calculateDeltaPMbar(q, kv, downP, rho, T)
hyd-->>v: { deltaPMbar, details }
v->>v: write pressure.predicted.delta
v->>parent: emitter.emit('deltaPChange', deltaP)
v->>out: notifyOutputChanged (Port 0 delta)
parent->>v: data.flow {variant, value, position, unit}
v->>v: MeasurementRouter.updateFlow → updateDeltaP
```
### Mode + source allow-lists
Each input is gated in `flowController.handleInput`:
```js
if (!this.isValidSourceForMode(source, this.host.currentMode)) {
this.logger.warn(`Source '${source}' is not valid for mode '${currentMode}'.`);
return { status: false, feedback: msg };
}
```
Defaults (per `valve.json` `mode.allowedSources`):
| Mode | Allowed sources |
|:---|:---|
| `auto` | `parent, GUI, fysical` |
| `virtualControl` | `GUI, fysical` |
| `fysicalControl` | `fysical` |
| `maintenance` | _(no entry &mdash; no source accepted; only `statusCheck` action allowed)_ |
A rejected request logs at warn and short-circuits.
> [!NOTE]
> Unlike `rotatingMachine`, `valve`'s `flowController` does not currently gate by `allowedActions` &mdash; only by source. The schema defines `mode.allowedActions` but it isn't enforced in `flowController.handleInput`. TODO: confirm intentional or backlog. Source: `nodes/valve/src/flow/flowController.js` lines 18&ndash;24.
---
## Output ports
| Port | Carries | Sample shape |
|:---|:---|:---|
| 0 (process) | Delta-compressed state snapshot &mdash; FSM state, position %, mode, every populated MeasurementContainer slot | `{topic, payload: {state, percentageOpen, moveTimeleft, mode, delta_predicted_pressure, downstream_measured_flow, ...}}` |
| 1 (telemetry) | InfluxDB line-protocol payload (same fields as Port 0) | `valve,id=valve_a state="operational",percentageOpen=60,delta_predicted_pressure=84,...` |
| 2 (register / control) | `child.register` upward at startup; `positionVsParent` and optional `distance` carried on the msg | `{topic: 'child.register', payload: <node.id>, positionVsParent, distance}` |
Port-0 key shape is **`<position>_<variant>_<type>`** (legacy three-segment). Examples: `delta_predicted_pressure`, `downstream_measured_flow`, `downstream_predicted_pressure`. Only keys with finite values are emitted &mdash; consumers must cache and merge.
On `query.curve` the node additionally emits `{topic: 'Showing curve', payload: <SupplierCurvePredictor.snapshot()>}` synchronously on Port 0.
See [EVOLV &mdash; Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) for the full InfluxDB layout.
---
## Event sources
| Source | Where it fires | What it triggers |
|:---|:---|:---|
| `state.emitter` `'positionChange'` | `movementManager` during a position move | `updatePosition()` &mdash; Kv lookup, deltaP recompute, Port 0 |
| `state.emitter` `'stateChange'` | `stateManager.transitionTo` resolve | Logged; `getOutput()` picks up the new `state` value on the next tick |
| `source.emitter` `'deltaPChange'` | `MeasurementRouter.updateDeltaP` after a finite deltaP | Consumed by `valveGroupControl` to update group totals |
| `source.emitter` `'fluidCompatibilityChange'` | `FluidCompatibility` on upstream-source contract change | Consumed by parent for service-type aggregation |
| `source.emitter` `'fluidContractChange'` | `FluidCompatibility` when the contract this valve advertises downstream changes | Consumed by downstream consumers |
| `source.measurements.emitter` `'<type>.<variant>.<position>'` | MeasurementContainer write | Generic handshake; parents subscribe via `child.measurements.emitter.on` |
| Inbound `msg.topic` | Node-RED input wire | `commandRegistry` dispatch |
| `setInterval(statusInterval = 1000)` | `BaseNodeAdapter` | Status badge re-render |
No per-second tick on the domain itself. Position moves drive their own animation interval inside `movementManager`.
---
## Where to start reading
| If you're changing... | Read first |
|:---|:---|
| Kv curve load / inline-curve fallback | `src/curve/supplierCurve.js` |
| Liquid / gas deltaP math, choke cap | `src/hydraulicModel.js` |
| Measurement &rarr; deltaP plumbing (when a recompute fires) | `src/measurement/measurementRouter.js` |
| Position-tick &rarr; updatePosition wiring | `src/state/stateBindings.js` |
| Mode allow-list, setpoint, executeSequence, pre-shutdown ramp | `src/flow/flowController.js` |
| Upstream-source fluid tracking, contract aggregation | `src/fluid/fluidCompatibility.js` |
| `query.curve` reply / status badge / Port 0 shape | `src/io/output.js` |
| Topic registration, payload validation, aliases | `src/commands/{index, handlers}.js` |
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + config + child filters |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues and open questions |
| [valveGroupControl wiki](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home) | The grouped-control parent |
| [EVOLV &mdash; Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |

254
wiki/Reference-Contracts.md Normal file
View File

@@ -0,0 +1,254 @@
# Reference &mdash; Contracts
![code-ref](https://img.shields.io/badge/code--ref-8c2b2c0-blue)
> [!NOTE]
> 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.
<!-- 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 `valve.json``mode.current`). |
| `cmd.startup` | — | any | — | Initiate the valve startup sequence. |
| `cmd.shutdown` | — | any | — | Initiate the valve shutdown sequence. |
| `cmd.estop` | `emergencystop`, `emergencyStop` | any | — | Trigger an emergency stop on the valve. |
| `execSequence` | — | `object` | — | Legacy umbrella that demuxes payload.action to startup / shutdown / estop. |
| `set.position` | `execMovement` | `object` | — | Move the valve to a control-% position via execMovement. |
| `data.flow` | `updateFlow` | `object` | — | Push a measured flow into the valve (variant + position + unit). |
| `query.curve` | `showcurve` | any | — | Return the valve characteristic curve on the reply port. |
| `child.register` | `registerChild` | `string` | — | Register a child measurement with this valve. |
<!-- END AUTOGEN: topic-contract -->
### `execSequence` demux
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 &mdash; 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}'.`);
return { status: false, feedback: msg };
}
```
Defaults from `valve.json`:
| Mode | `allowedSources` | `allowedActions` (schema) |
|:---|:---|:---|
| `auto` | `parent, GUI, fysical` | `statusCheck, execMovement, execSequence, emergencyStop` |
| `virtualControl` | `GUI, fysical` | `statusCheck, execMovement, execSequence, emergencyStop` |
| `fysicalControl` | `fysical` | `statusCheck, emergencyStop` |
| `maintenance` | _(none)_ | `statusCheck` |
> [!NOTE]
> `flowController.handleInput` currently enforces only the source side. The schema's `allowedActions` is defined but not gated in code &mdash; 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 &mdash; `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 | &mdash; | `"operational"` &mdash; one of the FSM states. |
| `percentageOpen` | number | % | `60` &mdash; current position `0..100`. |
| `moveTimeleft` | number | s | `0` &mdash; seconds remaining on the current move. |
| `mode` | string | &mdash; | `"auto"` / `"virtualControl"` / `"fysicalControl"` / `"maintenance"`. |
| `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. |
<!-- END AUTOGEN -->
### Status badge
`buildStatusBadge` in `io/output.js`:
```
<mode>: <state-symbol> <position%>% 💨<flow><unit> ΔP<deltaP> <unit>
```
(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 &mdash; 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()`. |
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
| Default unit | `general.unit` | `m3/h` (schema) | Re-resolved to the unitPolicy `output.flow` 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` | `atEquipment` | One of `atEquipment` / `upstream` / `downstream`. Used in the child-register payload that goes UP to VGC. |
| (hidden) | `functionality.softwareType` | `valve` | Constant. |
| (hidden) | `functionality.role` | `controller` | Constant. |
### Asset (`config.asset`)
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. |
| Tag code | `asset.tagCode` | `null` | |
| Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | |
| 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. |
| Accuracy | `asset.accuracy` | `null` | Optional. |
| Valve curve | `asset.valveCurve` | `{ '1.204': { '1': { x: [0..100 by 10], y: [0,18,50,95,150,216,337,564,882,1398,1870] } } }` | Nested map: outer key = density (kg per nm³), middle = diameter (mm), leaf = `{x: position%, y: Kv (m³/h)}`. |
Runtime options that bypass `config` and reach `configure()` via `Valve._pendingExtras.runtimeOptions`:
| UI field | runtimeOptions key | Default | Effect |
|:---|:---|:---|:---|
| 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 &mdash; 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` &mdash; **non-interruptible** safety. |
| Shutdown Time | `time.stopping` | Time in `stopping`. |
| Cooldown Time | `time.coolingdown` | Time in `coolingdown` &mdash; **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&ndash;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:
| Sequence | States |
|:---|:---|
| `startup` | `[starting, warmingup, operational]` |
| `shutdown` | `[stopping, coolingdown, idle]` |
| `emergencystop` | `[emergencystop, off]` |
| `boot` | `[idle, starting, warmingup, operational]` |
Note: unlike `rotatingMachine`, `valve.json` does not ship `entermaintenance` / `exitmaintenance` sequences. TODO: confirm whether maintenance transitions are intentionally manual.
### Mode (`config.mode`)
| Form field | Config key | Default | Range |
|:---|:---|:---|:---|
| Mode | `mode.current` | `auto` | `auto` / `virtualControl` / `fysicalControl` / `maintenance` |
| (defaults) | `mode.allowedActions.<mode>` | see [Topic contract](#mode--source-allow-lists) | schema only &mdash; not currently gated in code |
| (defaults) | `mode.allowedSources.<mode>` | see above | enforced by `flowController.isValidSourceForMode` |
### Unit policy
Source: `src/specificClass.js` lines 20&ndash;24.
| Quantity | Canonical (internal) | Output (rendered) | Required-unit |
|:---|:---|:---|:---:|
| Pressure | `Pa` | `mbar` | ✓ |
| Flow | `m3/s` | `m3/h` | ✓ |
| Temperature | `K` | `C` | ✓ |
`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` &mdash; declared in the schema. TODO: confirm whether the dispatch path consults `calculationMode` for trigger frequency. Source: `valve.json` lines 346&ndash;366.
---
## Child registration
Source: `src/specificClass.js` lines 100&ndash;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` |
| `machinegroup` / `machinegroupcontrol` | Same; recomputes aggregate service type. | `fluidContractChange` |
| `pumpingstation` | Same. | `fluidContractChange` |
| `valvegroupcontrol` | Same. | `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.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, FSM, hydraulic-model pipeline |
| [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 |

143
wiki/Reference-Examples.md Normal file
View File

@@ -0,0 +1,143 @@
# Reference &mdash; Examples
![code-ref](https://img.shields.io/badge/code--ref-8c2b2c0-blue)
> [!NOTE]
> Every example flow shipped under `nodes/valve/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/valve/examples/`.
>
> Pending full node review (2026-05). The shipped flows are currently minimal stubs; tiered demos (matching the `rotatingMachine` template) are on the backlog.
---
## Shipped examples
| File | Tier | Dependencies | What it shows |
|:---|:---:|:---|:---|
| `basic.flow.json` | 1 (stub) | EVOLV only | Minimal: one `inject` &rarr; one `valve` &rarr; one `debug`. Sanity check that the node loads. |
| `integration.flow.json` | 2 (stub) | EVOLV only | Same shape as basic; placeholder for VGC + measurement integration. |
| `edge.flow.json` | 3 (stub) | EVOLV only | Placeholder for edge cases (gas-choke, e-stop, invalid setpoints). |
> [!IMPORTANT]
> **Tiered example flows TODO.** Replace the three stubs with `01 - Basic Manual Control.json` / `02 - Integration with Valve Group.json` / `03 - Dashboard Visualization.json` following the `rotatingMachine` template. Track in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` and validate live against Docker-stack Node-RED. Screenshots / GIFs land under `wiki/_partial-screenshots/valve/` and `wiki/_partial-gifs/valve/`.
---
## Loading a flow
### Via the editor
1. Open the Node-RED editor at `http://localhost:1880`.
2. Menu &rarr; Import &rarr; drag the JSON file.
3. Click Deploy.
### Via the Admin API
```bash
curl -X POST -H 'Content-Type: application/json' \
--data @"nodes/valve/examples/basic.flow.json" \
http://localhost:1880/flow
```
Use `POST /flow` (single tab, full replace) or `POST /flows` (full deploy) depending on whether other tabs are already loaded.
---
## Driving the basic flow manually
The shipped `basic.flow.json` has a single `inject` wired to the valve. To exercise the FSM + hydraulic model, send the following sequence by hand (e.g. via additional inject nodes you wire in, or the Admin API):
1. `set.mode` &mdash; payload `"virtualControl"` &mdash; lets the GUI source drive the valve.
2. `cmd.startup` &mdash; payload `{}`. FSM walks `idle &rarr; starting &rarr; warmingup &rarr; operational`. Watch `state` on Port 0.
3. `set.position` &mdash; payload `{"setpoint": 60}`. FSM goes `operational &rarr; accelerating &rarr; operational`; `percentageOpen` ramps 0 &rarr; 60 at `movement.speed` %/s.
4. `data.flow` &mdash; payload `{"variant": "measured", "value": 25, "position": "downstream", "unit": "m3/h"}`. Flow lands in MeasurementContainer; `MeasurementRouter.updateFlow` recomputes deltaP. `delta_predicted_pressure` appears on Port 0; `evt.deltaPChange` fires upward.
5. `data.flow` &mdash; payload `{"variant": "measured", "value": 0, "position": "downstream", "unit": "mbar"}` to push downstream pressure as well (needed for the gas-flow path).
6. `cmd.shutdown` &mdash; payload `{}`. Because the valve is `operational`, the controller first ramps `percentageOpen` to 0, then `state` transitions `stopping &rarr; coolingdown &rarr; idle`.
> [!IMPORTANT]
> **GIF needed.** Demo recording of steps 1&ndash;6 + status badge progression. Save as `wiki/_partial-gifs/valve/01-basic-demo.gif`, target &le; 1&nbsp;MB after `gifsicle -O3 --lossy=80`.
### Try the position-residue handler
After the valve reaches `operational` at 60 %:
1. Send `set.position = {setpoint: 20}`. State goes `operational &rarr; decelerating &rarr; ...`.
2. While `decelerating`, send `set.position = {setpoint: 80}`.
3. `state.moveTo` recognises the residue state, transitions back to `operational` synchronously, then ramps up to 80. No setpoint is lost.
This is the same residue mechanism `rotatingMachine` uses for fast retargets.
### Try the e-stop sequence
From `operational`, send `cmd.estop`. The valve runs the `emergencystop` sequence (`[emergencystop, off]`). Allowed transitions out of `emergencystop` are `idle` / `off` / `maintenance`. To restart, drop to `idle` first (`cmd.shutdown` from `off` may not work depending on the state graph &mdash; TODO: confirm).
---
## Integration with `valveGroupControl`
> [!IMPORTANT]
> **TODO: Tier-2 example.** A proper integration flow with `valveGroupControl` + 2&times;valve children + an upstream `rotatingMachine` / `pumpingStation` for fluid-contract tracking is on the backlog. Screenshot under `wiki/_partial-screenshots/valve/02-integration.png`.
When built, the integration flow will demonstrate:
- Auto-registration via Port 2 at deploy &mdash; each valve's `child.register` reaches the VGC; no manual wiring needed.
- Upstream-source registration &mdash; a `rotatingMachine` registered as a child of the valve feeds `getFluidContract()` into `FluidCompatibility`. Status flips from `pending` / `compatible` / `mismatch` based on `serviceType` agreement.
- `evt.deltaPChange` propagation from each valve to the VGC for group-level deltaP aggregation.
---
## Dashboard visualization
> [!IMPORTANT]
> **TODO: Tier-3 example.** A FlowFuse Dashboard 2.0 page (`@flowfuse/node-red-dashboard`) with control buttons (mode, startup, shutdown, e-stop, position slider), live status (state badge, position %, deltaP, flow), and trend charts (deltaP, position) is on the backlog. Save as `03 - Dashboard Visualization.json`.
---
## 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 |
|:---|:---|:---|
| Editor throws `legacy asset field(s) [supplier, ...]` on deploy | Flow predates the AssetResolver refactor. Re-open the node, pick the model from the asset menu, save. The registry derives supplier / category / type. | `src/nodeClass.js` `_rejectLegacyAssetFields`. |
| Status badge shows `⚠ <message>` (yellow ring) | `getFluidCompatibility().status` is `mismatch` or `conflict`. An upstream source advertised a service type that doesn't match this valve's expected type. | `src/fluid/fluidCompatibility.js`, `getFluidCompatibility()`. |
| `delta_predicted_pressure` stuck at `0` or missing | `kv` is 0 (valve closed), the FSM isn't in `operational` / `accelerating` / `decelerating`, or no flow has landed. For gas flow, also needs a finite `downstream_measured_pressure`. | `state.getCurrentState()`, `percentageOpen`, `MeasurementRouter.updateDeltaP`. |
| `set.position` has no effect | Source not in `mode.allowedSources[currentMode]`. Watch for `Source '...' is not valid for mode '...'` in the warn log. | `src/flow/flowController.js` `isValidSourceForMode`. |
| `data.flow` payloads aren't reflected on Port 0 | Payload shape: `{variant: 'measured'\|'predicted', value: <number>, position: <string>, unit?: 'm3/h'}`. Missing `variant` warns `Unrecognized variant '...' for flow update`. Missing `value` warns `Received null or undefined value for flow update`. | `src/measurement/measurementRouter.js` `updateFlow`. |
| Gas-flow deltaP saturates at a ceiling | The choked-flow cap fired (`isChoked: true` in `hydraulicDiagnostics`). Increase `gasChokedRatioLimit` or revise downstream pressure. | `src/hydraulicModel.js` `_calculateGasDeltaP`. |
| `query.curve` returns empty `valveCurve` | `asset.model` not found by `assetResolver`; the predictor falls back to inline `asset.valveCurve` &mdash; check that exists. | `src/curve/supplierCurve.js` `SupplierCurvePredictor.snapshot()`. |
| FSM stuck in `accelerating` / `decelerating` | A move was aborted with `returnToOperationalOnAbort = false`. Send a new `set.position` &mdash; the residue handler in `state.moveTo` transitions back to `operational` first. | `generalFunctions/src/state/state.js` `moveTo` residue branch. |
| Per-valve Port 0 key names differ from what your dashboard expects | valve uses `<position>_<variant>_<type>` (e.g. `delta_predicted_pressure`, `downstream_measured_flow`). `rotatingMachine` uses `<type>.<variant>.<position>.<childId>`. Don't mix them. | `src/io/output.js` `buildOutput`. |
> Never ship `enableLog: 'debug'` in a demo &mdash; fills the container log within seconds and obscures real errors.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + config + child filters |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, FSM, hydraulic-model pipeline |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues and open questions |
| [valveGroupControl &mdash; Examples](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Reference-Examples) | Group-control demo flows |
| [EVOLV &mdash; Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where valve fits in a larger plant |

View File

@@ -0,0 +1,111 @@
# Reference &mdash; Limitations
![code-ref](https://img.shields.io/badge/code--ref-8c2b2c0-blue)
> [!NOTE]
> What `valve` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` in the superproject.
>
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
---
## When you would not use this node
| Scenario | Use instead |
|:---|:---|
| A fixed-restriction orifice (no actuator, no curve) | Model the deltaP externally; valve assumes a position-controlled Kv lookup. |
| A non-return / check valve (no motorised actuation) | Don't use valve &mdash; no FSM-driven position control is exposed for this case. |
| A pump or compressor (rotating equipment on a Q&ndash;H curve) | `rotatingMachine` &mdash; it loads a flow / power curve and predicts the operating point. |
| A throttling device with no known Kv curve | Without `asset.valveCurve` or a registry-resolved model, the predictor stays empty and deltaP recomputes return null. There is no fallback hydraulic model. |
| A grouped valve manifold | `valveGroupControl` &mdash; instantiate this as a child. |
---
## Known limitations
### Gas-choke detection is a hard cap
`hydraulicModel.js` `_calculateGasDeltaP` caps the effective deltaP at `gasChokedRatioLimit * P2_abs` once the raw deltaP exceeds that threshold. The result is a discontinuous step in deltaP &mdash; chart traces show a sharp ceiling rather than a smooth choked-flow transition. The `isChoked` flag in `hydraulicDiagnostics` lets consumers detect the regime. Tracked.
### Single-source pressure for the deltaP recompute
`MeasurementRouter` looks for `pressure.measured.downstream` (preferred) or `pressure.predicted.downstream` to feed the gas-flow formula. There is no fallback if both are missing &mdash; the hydraulic model returns `null` and `delta_predicted_pressure` simply doesn't get written. The liquid-flow path doesn't need downstream pressure. Tracked.
### Multi-parent registration
Allowed but not exercised in production tests. valve overrides `BaseDomain.registerChild` with `FluidCompatibility.registerChild`, which records upstream sources and aggregates `serviceType`. Teardown ordering (parent gone first vs valve gone first) is not test-covered. Open question.
### `flowController.handleInput` only gates by source, not action
The schema's `mode.allowedActions` is defined but `flowController.handleInput` only enforces `isValidSourceForMode`. A `cmd.shutdown` from a source allowed for the current mode will fire regardless of whether the schema lists `execSequence` in `allowedActions[mode]`. This differs from `rotatingMachine`, which gates both. TODO: confirm whether this is intentional (valve's reduced operational set) or a backlog item.
### `execSequence` legacy umbrella
The `execSequence` topic (with `payload.action = 'startup' | 'shutdown' | 'emergencystop'`) is kept alive for legacy flows. The handler demuxes to the canonical topic; the deprecation warning fires once per session. Scheduled for removal in Phase 7. Use `cmd.startup` / `cmd.shutdown` / `cmd.estop` instead.
### `data.flow` payloads don't clear stale values
If a flow source stops emitting, the last-known value persists in MeasurementContainer. There is no TTL and no explicit clear topic. Workaround: send `value: 0` explicitly. The deltaP recompute then writes 0 (or `null` if it short-circuits on `flow === 0`). Tracked.
### Editor cosmetics don't reflect `asset` derivation
The editor form may still expose supplier / category / type fields even though `_rejectLegacyAssetFields` rejects them on save. Re-saving an old flow surfaces the legacy-fields error until each valve is re-opened and the model re-picked. Cosmetic; the registry is the source of truth.
### No `entermaintenance` / `exitmaintenance` sequences out of the box
`valve.json` ships `startup` / `shutdown` / `emergencystop` / `boot` sequences only. `maintenance` is reachable via `set.mode = maintenance`, but there is no canned state-transition sequence for entering / leaving. `rotatingMachine` defines these in its own config &mdash; valve does not. TODO: confirm intentional.
### Position residue handling depends on shared state machine
The residue handler that lets a mid-decel `set.position` re-engage cleanly lives in `generalFunctions/src/state/state.js` `moveTo`. valve inherits the behaviour but the integration test coverage is thin. The sequence-abort token mechanism (`rotatingMachine`, 2026-05-15) may or may not apply to valve's shutdown ramp; TODO: confirm from `FlowController.executeSequence` behaviour when a new `set.position` arrives during the pre-shutdown ramp-to-zero.
---
## Open questions (tracked)
| Question | Where it lives |
|:---|:---|
| Gate `mode.allowedActions` in `flowController.handleInput` for parity with `rotatingMachine`? | Internal |
| Add `entermaintenance` / `exitmaintenance` sequences to `valve.json`? | Internal |
| Multi-parent teardown ordering | Internal |
| Add an explicit `data.clear-flow` topic for stale flow cleanup | Internal |
| Smooth choked-flow transition instead of a hard cap | `hydraulicModel.js` |
| Does the sequence-abort token mechanism (rotatingMachine, 2026-05-15) apply to valve's pre-shutdown ramp? | Internal &mdash; flagged in [Architecture](Reference-Architecture#fsm) |
| Phase 7 removal of `execSequence` umbrella + legacy aliases | Internal |
| `calculationMode` (`low` / `medium` / `high`) &mdash; does the dispatch path consult it? | `valve.json` schema vs source |
---
## Migration notes
### From pre-AssetResolver
Old flows saved with `supplier`, `category`, or `assetType` fields will throw on deploy:
```
valve: legacy asset field(s) [supplier, category] are saved on this node.
After the AssetResolver refactor these are derived from the model id.
Open the node in the editor, re-select the model, and save to migrate.
```
The fix is mechanical: open each valve node, re-pick the model from the asset menu, save. No data is lost &mdash; the registry has the same supplier / category / type the old flow carried.
### From inline `valveCurve` only
valve still supports `asset.valveCurve` as a fully inline curve fallback when no `asset.model` is set. The predictor logs which `(densityKey, diameterKey)` lane of the dataset it selected. If you migrate from inline-only to a registry-resolved model, the curve may pick a slightly different lane and your operating-point predictions will shift &mdash; review on the dashboard before declaring done.
### From `setpoint` topic name (pre-canonical)
The old topic without a `set.` prefix has been retired. Use `set.position` (alias `execMovement`) for position setpoints.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + config + child filters |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, FSM, hydraulic-model pipeline |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [valveGroupControl &mdash; Limitations](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Reference-Limitations) | Where the parent's aggregator sits |

19
wiki/_Sidebar.md Normal file
View File

@@ -0,0 +1,19 @@
### valve
- [Home](Home)
**Reference**
- [Contracts](Reference-Contracts)
- [Architecture](Reference-Architecture)
- [Examples](Reference-Examples)
- [Limitations](Reference-Limitations)
**Related**
- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home)
- [valveGroupControl wiki](https://gitea.wbd-rd.nl/RnD/valveGroupControl/wiki/Home)
- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home)
- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns)
- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)