# Reference — Contracts ![code-ref](https://img.shields.io/badge/code--ref-26e92b5-blue) > [!NOTE] > Full topic contract, configuration schema, and child-registration filters for `machineGroupControl`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/machineGroupControl.json`. > > For an intuitive overview, return to the [Home](Home). --- ## Topic contract The MGC accepts three canonical topics. `set.demand` is the only one with semantic content; the other two are simple state changes. | Canonical topic | Aliases | Payload | Unit handling | Effect | |:---|:---|:---|:---|:---| | `set.mode` | `setMode` | `string` (`"optimalControl"` \| `"priorityControl"` \| `"maintenance"`) | — | Switch the dispatch strategy. `maintenance` is monitoring-only — the dispatch switch warns and skips. | | `set.demand` | `Qd` | bare number, OR `{value: number, unit: string}` | self-describing (see below) | Operator demand setpoint. Resolves to canonical m³/s, then enters the latest-wins gate. Negative value = stop all (any unit). | | `child.register` | `registerChild` | `string` (Node-RED node id) | — | Register a child machine manually. Port 2 wiring does this automatically in normal flows. | ### `set.demand` — unit-self-describing semantics `src/commands/handlers.js` `setDemand`. The payload itself decides the meaning: | Payload form | Interpretation | |:---|:---| | `42` (bare number) | 42 %. Mapped through `interpolation.interpolate_lin_single_point(value, 0, 100, dt.flow.min, dt.flow.max)` to a canonical m³/s, clamped to the dynamic envelope. | | `{value: 42, unit: '%'}` | Same as above — explicit-percent form. | | `{value: 80, unit: 'm3/h'}` (or `l/s` / `m3/s` / …) | Absolute flow. Converted via `convert(value).from(unit).to('m3/s')`. | | `42` or `{value: …, unit: 'm3/h'}` with `value < 0` | Triggers `turnOffAllMachines()` regardless of unit. | | Anything else (`NaN`, missing) | Logged at error level; dispatch is skipped. | There is **no persistent `scaling` state** on the orchestrator. Each `set.demand` carries its own unit context; callers can switch between absolute and percent at will. After a successful dispatch the handler replies on the input port with `{topic: , payload: 'done'}` — the legacy "done" handshake some downstream flows still rely on. --- ## Data model — `getOutput()` shape Composed each tick by `src/io/output.js` `getOutput()` and emitted via `outputUtils.formatMsg` on Port 0. Delta-compressed: consumers see only the keys that changed. ### Per-measurement keys For every `(type, variant)` MeasurementContainer pair, the formatter emits **up to four keys** — one per position plus a differential when both upstream and downstream are present: ``` __ ``` Examples (with `variant=predicted`, `type=flow`): | Key | Source | |:---|:---| | `downstream_predicted_flow` | Group aggregate at the discharge side. | | `atEquipment_predicted_flow` | Optimizer intent (what the controller's solving for). | | `upstream_predicted_flow` | Group suction-side aggregate (when populated). | | `differential_predicted_flow` | `downstream − upstream` when both legs read. | Same shape for `pressure`, `power`, `temperature`, `efficiency`, `Ncog`. Output units are taken from the unit policy (`flow=m3/h`, `pressure=mbar`, `power=kW`, `temperature=°C`). ### Scalar group keys | Key | Type | Source | Notes | |:---|:---|:---|:---| | `mode` | string | `mgc.mode` | Current dispatch mode. | | `scaling` | (legacy) | `mgc.scaling` | Always `undefined` in the current code — the orchestrator no longer carries a scaling field. Kept in the formatter for now; will be removed. | | `absDistFromPeak` | number | `mgc.efficiency.calcDistanceBEP` | Absolute η distance to the group "peak" (mean of per-pump cogs). | | `relDistFromPeak` | number \| undefined | same | Normalised 0..1; `undefined` when the η spread collapses (homogeneous pump group). | | `headerDiffPa` | number | `mgc.operatingPoint.headerDiffPa` | Last header differential the equaliser resolved. Pa. | | `headerDiffMbar` | number | derived | Only emitted when `output.pressure === 'mbar'`. | | `flowCapacityMax` / `flowCapacityMin` | number | `mgc.dynamicTotals.flow.{max,min}` | The group's current envelope at the active header pressure. | | `machineCount` | number | `Object.keys(mgc.machines).length` | All registered children. | | `machineCountActive` | number | derived | Children whose state ≠ `off` / `maintenance` and currentMode ≠ `maintenance`. | ### Status badge `src/io/output.js` `getStatusBadge()` composes: ``` · · Q=/ m³/h · P= kW · /x ``` Fill colour: `green` when any pump is available, `yellow` when machines are registered but all are off/maintenance, `grey` when no pumps are registered. --- ## Configuration schema — editor form to config keys Source of truth: `generalFunctions/src/configs/machineGroupControl.json` plus `nodeClass.buildDomainConfig`. ### General (`config.general`) | Form field | Config key | Default | Notes | |:---|:---|:---|:---| | Name | `general.name` | `Machine Group Configuration` | Human-readable label. | | (auto-assigned) | `general.id` | `null` | Node-RED node id; assigned at deploy. | | Default unit | `general.unit` | `m3/h` | Surfaces as the unit-policy output for `flow`. | | Enable logging | `general.logging.enabled` | `true` | Master logger 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. | | (hidden) | `functionality.softwareType` | `machinegroupcontrol` | Constant. | | (hidden) | `functionality.role` | `GroupController` | Constant. | | Distance offset | `functionality.distance` | `null` | Optional spatial offset; populated from the editor when `hasDistance` is enabled. | | Distance unit | `functionality.distanceUnit` | `m` | | | Distance description | `functionality.distanceDescription` | `""` | Free-text. | ### Output (`config.output`) | Form field | Config key | Default | Range | Notes | |:---|:---|:---|:---|:---| | Process Output | `output.process` | `process` | `process` / `json` / `csv` | Port-0 formatter. | | Database Output | `output.dbase` | `influxdb` | `influxdb` / `json` / `csv` | Port-1 formatter. | ### Mode (`config.mode`) | Form field | Config key | Default | Range | Where used | |:---|:---|:---|:---|:---| | Control mode | `mode.current` | `optimalControl` | `optimalControl` / `priorityControl` / `maintenance` | dispatch switch in `_runDispatch`; mode-source/-action gates in `commands/handlers.js`. | | (defaults) | `mode.allowedActions.optimalControl` | `[statusCheck, execOptimalCombination, balanceLoad, emergencyStop]` | — | Enforced at command-handler entry via `specificClass.isValidActionForMode`. | | (defaults) | `mode.allowedActions.priorityControl` | `[statusCheck, execSequentialControl, balanceLoad, emergencyStop]` | — | Same. | | (defaults) | `mode.allowedActions.maintenance` | `[statusCheck]` | — | Same — dispatch/emergencyStop are dropped with a warn log. | | (defaults) | `mode.allowedSources.optimalControl` | `["parent","GUI","physical","API"]` | — | Enforced via `specificClass.isValidSourceForMode`. | | (defaults) | `mode.allowedSources.priorityControl` | `["parent","GUI","physical","API"]` | — | Same. | | (defaults) | `mode.allowedSources.maintenance` | `["parent","GUI"]` | — | Physical/HMI and API writes dropped in maintenance — monitoring only. | > [!NOTE] > `mode.current` is normalised at write time by `specificClass.setMode`: legacy lowercase inputs (`optimalcontrol`, `prioritycontrol`) are accepted and stored as the canonical camelCase. The `_runDispatch` switch then lowercases for its comparison — both forms reach the correct branch. Garbage modes (e.g. `'wat'`) are rejected with a warn log and the previous mode is preserved. > > Selecting `maintenance` no longer reaches `_runDispatch` at all in normal operation: the mode-action gate at `commands/handlers.js` drops the incoming `set.demand` before the dispatcher sees it. Status messages (`set.mode`, `child.register`) continue to flow. ### Unit policy Source: `src/specificClass.js` lines 33–37. | Quantity | Canonical (internal) | Output (rendered) | Required-unit | |:---|:---|:---|:---:| | Flow | `m3/s` | `m3/h` | ✓ | | Pressure | `Pa` | `mbar` | ✓ | | Power | `W` | `kW` | ✓ | | Temperature | `K` | `°C` | ✓ | `requireUnitForTypes` means MeasurementContainer rejects writes without an explicit unit for these types — protects against accidentally writing raw numbers in the wrong scale. --- ## Child registration Source: `src/specificClass.js` `configure()` lines 92–118. | softwareType | Filter / subscribed events | Side-effect | |:---|:---|:---| | `machine` | `onRegister` stores the child in `this.machines[id]`. Subscribes to `pressure.measured.downstream`, `pressure.measured.differential`, and `flow.predicted.downstream` from the child's emitter. | Every event calls `handlePressureChange()` — equalises the header, recomputes dynamic totals, refreshes group η, fires `notifyOutputChanged()`. | | `measurement` | `onRegister` reads `asset.type` and `positionVsParent`, subscribes to `.measured.` on the child's measurement emitter. | Mirrors the value into MGC's own MeasurementContainer; pressure values additionally trigger `handlePressureChange()`. | A child whose `asset.type` or `positionVsParent` is missing is logged at warn and skipped (not registered). There is **no filter on `machinegroup` / `pumpingstation` children** — MGC is a leaf controller; it parents pumps but doesn't accept fellow aggregators. --- ## Header-pressure equalisation Source: `src/groupOps/groupOperatingPoint.js` `equalize()`. MGC ensures every registered child uses the **same** header differential pressure when computing predicted flow / power. Algorithm: 1. Read MGC's own group-scope pressure (downstream and upstream) from its MeasurementContainer. 2. Read each child's measured pressure (downstream / upstream). 3. Pick: - `headerDownstream` = group reading if positive, else `max` across children. - `headerUpstream` = group reading if positive, else `min` across children. 4. If the differential is non-positive, skip the equalisation (debug log). 5. Stash the diff on `this.headerDiffPa` (used by `getOutput` and by every η computation). 6. Push the diff onto each child's `predictFlow.fDimension` / `predictPower.fDimension` / `predictCtrl.fDimension` — preferred path is `child.setGroupOperatingPoint(downstream, upstream)`, which lets the child re-build its `groupPredict*` interpolators. Older children fall back to a direct `fDimension` write. The equaliser is called from `handlePressureChange` (on every child pressure / predicted-flow event) and from the start of `_optimalControl`. --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Architecture](Reference-Architecture) | Code map, dispatch lifecycle, planner internals | | [Reference — Examples](Reference-Examples) | Shipped flows | | [Reference — Limitations](Reference-Limitations) | Known issues and open questions | | [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules | | [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |