feat(mgc): editor defaults, compact status badge, mode-case fix, real example flows + dashboard

Editor (mgc.html)
- Drag-in defaults now expose mode (optimalControl) and scaling (normalized)
  via dropdowns in the edit dialog. Was: no control fields in the UI at all,
  so users had to send set.mode/set.scaling after deploy or live with the
  hidden schema defaults.

Wire-up (src/nodeClass.js)
- buildDomainConfig now bridges the flat editor fields (mode, scaling) into
  the nested schema shape (mode.current, scaling.current). Was: returned {}
  so the editor's mode/scaling never reached the runtime.

Mode-case bug fix (src/specificClass.js)
- Schema enum values are camelCase (optimalControl, priorityControl) but the
  runtime switch in _runDispatch matched lowercase only. With the default
  config, dispatch silently fell through to the warning branch and nothing
  ran. Normalise via String(this.mode).toLowerCase() so both forms work.

Status badge (src/io/output.js)
- Compacted from ~80 chars (mode | Ⓝ: 💨=Q/Qmax | =P | N machine(s)) to
  ~50 chars (mode | norm | Q=Q/Qmax m³/h | P=P kW | active/total x).
  Drops emoji glyphs that rendered inconsistently across themes; uses the
  same dot+fill convention as pumpingStation.

Output extension (src/io/output.js)
- getOutput() now also emits flowCapacityMin/Max, machineCount,
  machineCountActive. Was: only group-level totals + dist-from-peak +
  mode/scaling, so dashboards couldn't show capacity / active count
  without subscribing to each rotatingMachine individually.

Examples
- Drop pre-refactor stubs (basic.flow.json, integration.flow.json,
  edge.flow.json). They had a single MGC + inject + debug, no children,
  and never dispatched anything.
- 01-Basic.json: 1 MGC + 3 rotatingMachine pumps + Setup once-fires
  virtualControl + cmd.startup on all pumps via fan-out function. Numbered
  driver groups for Control mode / Scaling / Operator demand. Pumps
  register with MGC via Port 2 (child.register, automatic).
- 02-Dashboard.json: same plumbing + FlowFuse Dashboard 2.0 page with
  Controls (mode + scaling buttons, demand slider 0–100, stop + init
  buttons), Status (7 ui-text rows), Trends (3 charts: flow + capacity,
  power, BEP rel %), and a raw-output ui-template dumping every Port 0
  field. Fan-out function caches last-known values so deltas don't blank.

Wiki + README
- examples/README.md rewritten for the two-file set with canonical command
  surface table and "what to try" recipes.
- wiki/Home.md §11 (Examples) updated; §14 #4 (TODO flow item) replaced
  with the actual current limitation (no per-pump fan-out on Port 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-12 15:24:03 +02:00
parent 05de4ee29a
commit 4cb9c5084c
11 changed files with 1996 additions and 43 deletions

View File

@@ -41,6 +41,18 @@ function getOutput(mgc) {
out.scaling = scaling;
out.absDistFromPeak = absDistFromPeak;
out.relDistFromPeak = relDistFromPeak;
// Group capacity + active-machine counts. Surfaced so dashboards can
// show the same numbers the status badge does without subscribing to
// every child node individually.
out.flowCapacityMax = mgc.dynamicTotals?.flow?.max ?? 0;
out.flowCapacityMin = mgc.dynamicTotals?.flow?.min ?? 0;
out.machineCount = Object.keys(mgc.machines || {}).length;
out.machineCountActive = Object.values(mgc.machines || {}).filter((m) => {
const s = m?.state?.getCurrentState?.();
const md = m?.currentMode;
return s && s !== 'off' && s !== 'maintenance' && md !== 'maintenance';
}).length;
return out;
}
@@ -55,15 +67,16 @@ function getStatusBadge(mgc) {
const md = m?.currentMode;
return s && s !== 'off' && s !== 'maintenance' && md !== 'maintenance';
});
const status = available.length > 0 ? `${available.length} machine(s)` : 'No machines';
let scalingSymbol;
switch ((mgc.scaling || '').toLowerCase()) {
case 'absolute': scalingSymbol = ''; break;
case 'normalized': scalingSymbol = 'Ⓝ'; break;
default: scalingSymbol = mgc.mode || ''; break;
}
const text = ` ${mgc.mode || 'Unknown'} | ${scalingSymbol}: 💨=${Math.round(totalFlow)}/${Math.round(totalCapacity)} | ⚡=${Math.round(totalPower)} | ${status}`;
return statusBadge.text(text, { fill: available.length > 0 ? 'green' : 'red', shape: 'dot' });
const machineCount = Object.keys(mgc.machines || {}).length;
const scaling = String(mgc.scaling || '').toLowerCase() === 'absolute' ? 'abs' : 'norm';
const parts = [
mgc.mode || '?',
scaling,
`Q=${Math.round(totalFlow)}/${Math.round(totalCapacity)} m³/h`,
`P=${Math.round(totalPower)} kW`,
`${available.length}/${machineCount}x`,
];
return statusBadge.compose(parts, { fill: available.length > 0 ? 'green' : (machineCount > 0 ? 'yellow' : 'grey'), shape: 'dot' });
}
module.exports = { getOutput, getStatusBadge };

View File

@@ -12,8 +12,14 @@ class nodeClass extends BaseNodeAdapter {
static tickInterval = null;
static statusInterval = 1000;
buildDomainConfig() {
return {};
buildDomainConfig(uiConfig = {}) {
// Schema shape is mode.current / scaling.current (the schema nests
// value + allowedActions/allowedSources under `current`). Editor field
// names are flat — bridge here.
const out = {};
if (uiConfig.mode) out.mode = { current: uiConfig.mode };
if (uiConfig.scaling) out.scaling = { current: uiConfig.scaling };
return out;
}
}

View File

@@ -260,8 +260,11 @@ class MachineGroup extends BaseDomain {
demandQout = this.interpolation.interpolate_lin_single_point(demandQ, 0, 100, dt.flow.min, dt.flow.max);
}
// Normalize for the switch — schema enum values use camelCase
// (optimalControl, priorityControl) while legacy callers send
// lowercase. Accept both rather than silently falling through.
const ctx = { mgc: this };
switch (this.mode) {
switch (String(this.mode || '').toLowerCase()) {
case 'prioritycontrol': await control.equalFlowControl(ctx, demandQout, powerCap, priorityList); break;
case 'prioritypercentagecontrol':
if (this.scaling !== 'normalized') { this.logger.warn('Priority percentage control needs normalized scaling.'); return; }