Waterschap Brabantse Delta · R&D-lab · uitleg

EVOLV · valveGroupControl — één groepsflow, n kleppen: zo verdeelt de code

eigenaar R&D-lab laatst getest 2026-07-17 versie 2.0 concept
valveGroupControl (VGC) is de S88-Unit-orkestrator die meerdere kind-ventielen (valve-nodes) als één groep bestuurt. Kern: hij verdeelt de totale groepsflow over de beschikbare kleppen naar rato van hun Kv met een korte iteratieve solve (max 2 passes, tolerantie 0,001) die per klep terugleest wat het kind écht accepteerde, en publiceert de maximale delta-P over de groep. Daarnaast aggregeert hij de fluid contracts (gas/liquid) van upstream bronnen tot één groepscontract. De node volgt het EVOLV-drielagenpatroon met de rekenlogica in aparte concern-modules. Deze pagina volgt letterlijk wat de code doet aan de hand van één voorbeeld en een interactieve figuur die de solve getrouw naspeelt.

Doel & rol in de keten

VGC is een S88-Unit (lane L4, kleur #50a8d9; node-CLAUDE.md, tabel "S88 classification"; commentaar in vgc.html:1-10). De parent-child-relatie werkt twee kanten op. Als parent registreert VGC twee klassen kinderen: valve-kinderen (in this.valves, src/specificClass.js:47,62-83) en upstream bronnen van de canonieke types machine/machinegroup/pumpingstation/valvegroupcontrol (in this.sources, src/specificClass.js:15,48-50; src/sources/fluidContract.js:111-123). Als child registreert VGC zichzelf bij zíjn eigen parent via één child.register-bericht op poort 2 bij opstart (generalFunctions/src/nodered/BaseNodeAdapter.js:117-132). Een VGC kan dus zelf weer bron of kind zijn van een andere VGC of een valve.

De node is tick-gedreven: de adapter zet static tickInterval = 1000 en roept elke seconde tick() = calcValveFlows() aan — een periodieke reconcile-pass die de per-ventiel-toewijzing hersynchroniseert als een kind is gedrift tussen event-driven herberekeningen (src/nodeClass.js:9-14; src/specificClass.js:246-249). Omdat de tick-modus actief is, koppelt de adapter de event-driven output-changed-listener niet aan (BaseNodeAdapter.js:137-153): alle notifyOutputChanged()-aanroepen in het domein zijn daardoor inert; output verschijnt maximaal 1 Hz via de tick, met delta-compressie erbovenop.

Rode draad door de hele pagina: een groep van 3 kleppen met Kv = 4 / 8 / 12; klep 1 verzadigt bij 10 m³/h (accepteert niet meer). Groepsvraag 90 m³/h. De verdeling en de iteratieve solve zijn met node tegen de échte VGC-code (src/groupOps/flowDistribution.js, functie distributeFlow) doorgerekend — verdeling, aantal passes, tolerantie en Kv-proportionaliteit geverifieerd; de klep-verzadiging is een illustratief kind-eigenschap (zie de figuur).

Praat met deze node

Het I/O-contract in één oogopslag: kies een verb, stel de velden in, kopieer het exacte injecteerbare msg, en klik Injecteer ▸ om te zien wat er uit de drie poorten komt — met live doorgerekende waarden op de voorbeeldgroep (3 kleppen, Kv 4/8/12, klep 1 verzadigt bij 10 m³/h). Elke bewering draagt een pad:regel-chip. Poortlabels ["process","dbase","parent"]. De console gebruikt meta.origin = parent (de default).

Stuurinput · 1 poort
Bericht
Poortenoutput · 3 poorten
Volledige poort-referentie
Poortcontract (labels ["process","dbase","parent"], vgc.html:39). Poort 0/1 gaan samen elke tick, delta-gecomprimeerd: is er niets gewijzigd, dan geen bericht (BaseNodeAdapter.js:156-163; generalFunctions/src/helper/outputUtils.js:17-67). Output-units: flow m3/h, druk mbar.
PoortLabelBelangrijkste keys
0process msg.topic = general.name (default "ValveGroupControl"). Payload: dynamische keys <position>_<variant>_<type>posities lowercase (MeasurementContainer.js:287). Door VGC zelf geschreven: atequipment_measured_flow, atequipment_predicted_flow (som toegewezen flows, flowDistribution.js:106), deltamax_predicted_pressure (flowDistribution.js:123); plus de scalars mode en maxDeltaP altijd in het ruwe object (src/io/output.js:16-31).
1dbase Zelfde velden via de influxdb-formatter (of frost/json/csv via dbaseOutputFormat, vgc.html:23,91-97), inclusief geflattende config-tags (id, softwareType, role, positionVsParent, uuid, tagcode, geoLocation, …); lege tags gedropt (outputUtils.js:54,87-133).
2parent Eenmalig bij opstart (na 100 ms): {topic:'child.register', payload:node.id, positionVsParent, distance} (BaseNodeAdapter.js:117-132); verder stuurt _emitOutputs er altijd null op.
Naast de poorten zijn er in-proces emitter-events (geen msg-poort): fluidContractChange (fluidContract.js:108), reconcileIntervalChange (src/specificClass.js:242) en per meetschrijfactie <type>.<variant>.<positie-lowercase> zoals flow.predicted.atequipment (CONTRACT.md:50-62).

Wie praat met wie

De parent/bron staat boven, de klep-kinderen onder. Registratie loopt via draden (poort 2): de kleppen registreren omhoog bij VGC, VGC registreert omhoog bij zijn parent. Telemetrie gaat via poort 0/1. De besturing naar de kleppen loopt in-process: VGC schrijft rechtstreeks valve.updateFlow('predicted', q, 'downstream') op de geregistreerde kind-objecten. De kleppen praten terug met in-proces events: positionChange en deltaPChange triggeren respectievelijk een herverdeling en een max-delta-P-herberekening.

parent / bron pumpingStation · MGC · VGC valveGroupControl deze node · S88 Unit valve · klep 1 Kv 4 · verzadigt @10 valve · klep 2 Kv 8 valve · klep 3 Kv 12 p2 registratie p0/p1 → proces / Influx p2 child.register (kleppen → VGC) flow-besturing · in-process (updateFlow) events: positionChange · deltaPChange
registratie · via draad (poort 2) telemetrie · via draad (poort 0/1) flow-besturing · in-process (updateFlow) in-proces events · valve-emitter

Kind-registratie: de klep stuurt {topic:'child.register', payload:<node-id>, positionVsParent} → VGC resolvet het object via RED.nodes.getNode(id).source en registreert via de ChildRouter (handlers.js:58-67; BaseDomain.js:65-68). _registerValve doet een duck-type-check (updateFlow, state.getCurrentState, measurements verplicht), weigert duplicaat-id's, en bindt de kind-events (src/specificClass.js:62-83,92-100). Bronnen abonneren op zes flow-events (flow.{predicted,measured}.{downstream,atEquipment,atequipment}) die de groepstotaalflow zetten en herverdeling triggeren (fluidContract.js:18-25,111-139).

De reis van één groepsflow

Klik een stap in de lijst (of gebruik ◀ ▶). Boven zie je waar in de pijplijn je bent; rechts wat er gebeurt, de waarde van het rode-draadvoorbeeld op dat punt (met node tegen flowDistribution.js doorgerekend), en het codepad.

Kv-proportionele verdeling & de iteratieve solve, interactief

De solve verdeelt de groepsvraag over de beschikbare kleppen naar rato van hun Kv, schrijft per klep direct een target (valve.updateFlow('predicted', q, 'downstream')), leest daarna terug wat elke klep accepteerde, en herverdeelt de resterende residualmaximaal 2 passes, of tot |residual| ≤ 0,001 m³/h (flowDistribution.js:40-75). Schuif de Kv's, de per-klep verzadigingsgrens (het maximale debiet dat het kind accepteert) en de groepsvraag; de balken tonen het Kv-proportionele ideaal, wat na pass 1 werd geaccepteerd, en de eindtoewijzing na pass 2. Verzadigt een klep, dan schuift de tweede pass de rest Kv-proportioneel naar de kleppen die nog ruimte hebben. De verzadigingsgrens is een illustratief kind-gedrag: in werkelijkheid komt "geaccepteerd < gevraagd" uit de valve-node zelf, die VGC via flow.predicted.downstream terugleest (_readAcceptedFlow, flowDistribution.js:29-38).

klep 1 4 10
klep 2 8 120
klep 3 12 120
90 m³/h
Solve-uitlezing (live)
Waarom soms een rest-residual?
De solve stopt na 2 passes. Accepteert een klep blijvend minder dan gevraagd, dan kan een rest-residual blijven staan — die wordt alleen in lastFlowSolve.residual bewaard, niet gealarmeerd, en zit niet in getOutput() (flowDistribution.js:74,107; output.js:16-31). Downstream dus onzichtbaar.

Toestanden & configuratie

Modi (this.currentMode, init uit config.mode.current default auto): auto | virtualControl | fysicalControl | maintenance (generalFunctions/src/configs/valveGroupControl.json:153-177). Per modus bepaalt allowedSources welke origin commando's mag geven (isValidSourceForMode, src/specificClass.js:144-147): auto → {parent, GUI, fysical}; virtualControl → {GUI, fysical}; fysicalControl → {fysical}. Voor maintenance bestaat géén allowedSources-entry — zie de aandachtspunten. De state wordt bij configure direct op operational gezet (src/specificClass.js:44-45); er zijn geen tijd-/overgangsconfiguraties per state. Sequenties (executeSequence, src/specificClass.js:162-173) lopen een geordende Set van statenamen af; defaults (valveGroupControl.json:267-308): startup = starting→warmingup→operational; shutdown = stopping→coolingdown→idle; emergencystop = emergencystop→off; boot = idle→starting→warmingup→operational.

Belangrijkste configuratievelden; volledig schema (309 regels) in nodes/generalFunctions/src/configs/valveGroupControl.json. De verdeel-/reconciliatieparameters maxPasses en residualTolerance zijn niet configureerbaar — hardcoded defaults (flowDistribution.js:8; src/specificClass.js:39).
SleutelDefaultBetekenis
general.name"ValveGroupControl"Label én msg.topic/measurement-naam (valveGroupControl.json:3-9).
general.unit"unitless"Bij configure overschreven naar de flow-outputunit m3/h (src/specificClass.js:27-29).
functionality.softwareType"valvegroupcontrol"Bepaalt router-dispatch bij registratie als kind van een andere parent (valveGroupControl.json:61-76).
mode.current"auto"Startmodus; mode.allowedSources = per modus een Set toegestane origins (valveGroupControl.json:153-212).
action (enum)statusCheckDocumentatieve actielijst; valvePositionChange/valveDeltaPchange staan erin maar zijn niet geïmplementeerd (zie aandachtspunten; valveGroupControl.json:234-266).
sequences.*SetsGeordende statelijsten voor startup/shutdown/emergencystop/boot (valveGroupControl.json:267-308).

Statusbadge (editor, elke 1000 ms): "<mode> | flow=<m³/h> m3/h | <n> valve(s) connected" — groen bij ≥1 beschikbare klep, rood bij geen (src/io/output.js:33-45).

Aandachtspunten uit de code-analyse

Alle punten zijn code-observaties d.d. 2026-07-17 op submodule valveGroupControl @ 13a14b882b63 (== origin/main; runtime-gedrag niet geverifieerd). De open-issue-lijst is niet geraadpleegd — onzeker of deze al als issue bekend zijn.

maintenance ⇒ TypeError
isValidSourceForMode doet (this.config.mode.allowedSources[mode] || []).has(source) (src/specificClass.js:144-147). Voor maintenance bestaat géén allowedSources-entry (valveGroupControl.json:183-208), de fallback is een Array, en Array.prototype.has bestaat niet. De aanroep staat vóór het try-blok van handleInput (src/specificClass.js:198-205), dus elke execSequence/totalFlow/emergencyStop in maintenance escaleert tot een exception die pas in de adapter wordt gevangen (BaseNodeAdapter.js:165-182). aanname: bedoeld is vermoedelijk "geen bronnen toegestaan" met een nette warn — dit is een code-observatie, geen geverifieerde runtime-crash.
emergencyStop is modus-gegate
De config-enum beschrijft emergencyStop als "Overrides all commands" (valveGroupControl.json:259-262), maar de handler loopt via handleInput en dus door de bron-check: in fysicalControl wordt een emergencyStop met origin parent/GUI geweigerd (handlers.js:37-39; src/specificClass.js:198-203), en in maintenance raakt hij de TypeError hierboven. Documentatie en implementatie sporen hier niet. hypothese: onbedoelde koppeling.
solve: side-effects, max 2 passes
Tussentargets worden tijdens de solve al naar de kinderen geschreven (flowDistribution.js:53-57); accepteert een kind minder dan gevraagd, dan kan na 2 passes een rest-residual blijven staan (interactieve figuur hierboven, rode draad: 0,833 m³/h). Alleen in lastFlowSolve.residual geregistreerd, niet gealarmeerd, en niet in getOutput() (flowDistribution.js:74,107; output.js:16-31) — downstream onzichtbaar.
dode actie-enums
De config-enum action bevat valvePositionChange en valveDeltaPchange (valveGroupControl.json:234-266), maar de switch in handleInput kent alleen execSequence/totalFlowChange/emergencyStop/ statusCheck (src/specificClass.js:207-231); onbekende acties vallen in de default-warn. Positie-/deltaP-wijzigingen lopen in werkelijkheid via child-events.
notifyOutputChanged inert
Omdat de tick-modus actief is (tickInterval = 1000), koppelt de adapter de output-changed-listener niet aan (BaseNodeAdapter.js:137-153); alle notifyOutputChanged()-aanroepen in het domein (src/specificClass.js:140,159) zijn daardoor in de praktijk inert — output loopt uitsluitend via de 1 Hz-tick met delta-compressie.
geen auth-guard · testgaten
De twee httpAdmin-GET's (GET /valveGroupControl/menu.js en …/configData.js) hebben géén RED.auth.needsPermission-guard (vgc.js:20-38); de repo-security-regel noemt die als standaard. Niet gedekt door tests: nul-Kv-pad, negatieve delta-P in calcMaxDeltaP, tick-gedrag, executeSequence, maintenance-gating; geen test/_output-manifest.md.

Gepland/transitional: het position-verb is een bewuste no-op "pending Phase 7" (index.js:28; handlers.js:21-23); child.register loopt transitional op het legacy msg.topic-pad (index.js:66-70); de rename vgc.{js,html}valveGroupControl.{js,html} staat gepland in de EVOLV-CLAUDE.md:51-54.

Verder lezen

Bron statische code-analyse van de werkkopie RnD/EVOLV (super-repo 74c4089, 2026-07-03) — submodule valveGroupControl @ 13a14b882b63, plus het feitendossier van deze analyse. Analysedatum 2026-07-17.
Versheids-herverificatie op 2026-07-17 is git fetch gedraaid: de geanalyseerde submodule-SHA 13a14b882b63 is gelijk aan origin/main (en aan de werkkopie-HEAD) — de analyse beschrijft de actuele tip.
Methode statische code-analyse (geen runtime-verificatie); elk feit draagt een pad:regel-verwijzing. De interactieve figuur en de console zijn getrouwe herimplementaties: de Kv-proportionele verdeling en de iteratieve solve zijn met node tegen de échte distributeFlow-code doorgerekend (aantal passes, tolerantie 0,001 en Kv-proportionaliteit geverifieerd; testgevallen als commentaar in de pagina-JS). Illustratieve aannames (o.a. de per-klep verzadigingsgrens) zijn expliciet zo gelabeld.
Beperking beschrijft de geanalyseerde revisie; gedrag op andere branches of deployments kan afwijken.
Contact R&D-lab · lab.wbd-rd.nl