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>
13 KiB
Reference — Contracts
Note
Pending full node review (2026-05). Content reflects
CONTRACT.mdand current source only.Full topic contract, configuration schema, and child-registration filters for
valveGroupControl. Source of truth:src/commands/index.js,src/specificClass.jsconfigure(), and the schema atgeneralFunctions/src/configs/valveGroupControl.json.For an intuitive overview, return to the 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 (auto / virtualControl / fysicalControl / maintenance) |
— | Switch operational mode via source.setMode(payload). Each mode has its own allow-list of sources (mode.allowedSources). |
set.position |
setpoint |
any | — | No-op pending Phase 7. Reserved for future per-valve positional override; the handler is debug-logged only. |
child.register |
registerChild |
string (child node id) |
— | Resolve via RED.nodes.getNode; if the child exposes .source, register through childRegistrationUtils.registerChild(child.source, msg.positionVsParent). |
cmd.execSequence |
execSequence |
{ source, action, parameter } |
— | Forward to source.handleInput(source, action, parameter). The action typically names a sequence; the parameter typically names the state list. |
data.totalFlow |
totalFlowChange |
number, { value, position?, variant?, unit? }, or { source, action, ... } |
volumeFlowRate (default m3/h) |
Update total measured/predicted flow at the configured position; drives calcValveFlows to re-distribute. If payload.source is present, route via handleInput(src, action, payload); otherwise treat as parent/totalFlowChange. |
cmd.emergencyStop |
emergencyStop, emergencystop |
optional { source } |
— | Run the emergencystop sequence via handleInput(src, 'emergencystop'). Default source is parent. |
set.reconcileInterval |
setReconcileInterval |
number — seconds (> 0) | seconds | Re-tune the periodic flow-reconciliation interval (setReconcileIntervalSeconds). Min clamp 100 ms. Non-finite or ≤ 0 logs a warn and is dropped. |
Mode / source allow-lists
A topic that survives the registry still passes through flowController → handleInput, which enforces:
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.mddescribesset.modeas switching between "auto / manual control modes", but the schema defines four modes (auto/virtualControl/fysicalControl/maintenance) andspecificClass.setModevalidates against the schema's enum. The wider four-mode set is the implementation. TODO: tighten the prose inCONTRACT.mdto enumerate the schema modes.
Warning
Source contradiction: the schema declares an
mode.allowedActionstable, but the running implementation only consultsisValidSourceForMode—isValidActionForModeis not implemented on VGC. Action allow-lists are effectively dead config. TODO: either implement the action check (mirroringrotatingMachine's pattern) or removeallowedActionsfrom 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:
<position>_<variant>_<type>
| Example key | Unit | Source | Notes |
|---|---|---|---|
atEquipment_measured_flow |
m³/h | upstream source flow.measured.* events; data.totalFlow with variant=measured |
Total measured flow at the group inlet. |
atEquipment_predicted_flow |
m³/h | written by distributeFlow as sum(accepted) |
Sum of per-valve accepted flows after Kv-share + residual. |
deltaMax_predicted_pressure |
mbar | written by calcMaxDeltaP |
Max pressure.predicted.delta across registered valves. |
Delta compression: only changed fields are sent per tick. Consumers must cache and merge. See
outputUtils.formatMsg.
Status badge
io/output.getStatusBadge:
<mode> | flow=<int> <flowUnit> | <N> valve(s) connected | (or 'No valves')
| State | Fill |
|---|---|
getAvailableValves().length > 0 |
green dot |
getAvailableValves().length === 0 |
red dot |
flow is the rounded flow.measured.atEquipment, or flow.predicted.atEquipment if no measured value is available.
Configuration schema — 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.<mode> |
see Mode allow-lists | enforced by flowController (NOT implemented — see warning above) |
|
| (defaults) | mode.allowedSources.<mode> |
see Mode 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.)
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
calculationModeis in the schema but is not currently consulted byspecificClassornodeClass. The tick interval is fixed attickInterval = 1000 msand only retunable throughset.reconcileInterval. TODO: wirecalculationModethrough 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 ` |
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. |
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 for the full reconciliation logic. The aggregated fluidContract is exposed via vgc.getFluidContract():
{
"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 | Intuitive overview |
| Reference — Architecture | Code map, flow-distribution loop, source aggregation |
| Reference — Examples | Shipped flows + debug recipes |
| Reference — Limitations | Known issues and open questions |
| EVOLV — Topic Conventions | Platform-wide topic rules |
| EVOLV — Telemetry | Port 0 / 1 / 2 InfluxDB layout |