diff --git a/docs/DESIGN_DELESTAGE.md b/docs/DESIGN_DELESTAGE.md new file mode 100644 index 0000000..7c620e2 --- /dev/null +++ b/docs/DESIGN_DELESTAGE.md @@ -0,0 +1,159 @@ +# DESIGN — l'extension du délestage au waterfall + +**Statut : conception, à valider avant tout code de liaison** (`AGENTS.md`, workflow obligatoire). +**Précondition de** : `§10` (LM-1006, couplage 1). Le §10 ne part pas sans elle. +**Contrainte de forme** : `specs/spec_loadmodel.md` **LM-1006-1**, écrite le 2026-08-27 +*« pour que la porte reste ouverte »*. Elle se referme ici. + +> **Ce document ne demande pas si le délestage doit s'étendre.** Il dit sous quelle forme, dans +> quel ordre, et nomme **ce qui demande une décision** — §6. + +--- + +## 1. L'écart, mesuré + +| | Aujourd'hui | +|---|---| +| **Qui est délesté** | `verifyOverloadProtection()` parcourt `m_evChargers` et **rien d'autre** | +| **Ce que le waterfall sait du plafond** | `evPhaseAllowanceW()` — et **uniquement pour les bornes**, comme plafond déclaré d'une borne | +| **Charges pilotées, PAC** | aucun plafond : leur `maxPowerW` vient de la configuration, la cascade n'est bornée que par le **surplus** | + +Tant que la cascade ne dépense que du surplus, l'absence de plafond ne se voit pas : on ne peut +pas dépasser ce qu'on n'achète pas. **Le §10 change cela** — la passe éco décide d'acheter, et +plusieurs planchers qui s'ouvrent le même matin sans soleil s'additionnent sur le disjoncteur de +branchement. + +--- + +## 2. Le mécanisme : UN plafond, TROIS sources + +LM-1006-1 l'impose et c'est le cœur du lot : **la source est un paramètre, jamais une hypothèse +enfouie dans le nom de la fonction.** + +``` +struct DrawCap { + Source source; // Breaker | GridOperator | EcoSelfImposed + double perPhaseW; // < 0 = ce plafond ne borne pas par phase + double totalW; // < 0 = ce plafond ne borne pas au total +}; +``` + +**Les deux formes coexistent et ne se convertissent JAMAIS** (LM-1006-1). Un plafond par phase +n'est pas un tiers d'un plafond total : une maison peut être à 20 A sur une phase et 2 A sur les +deux autres. + +| Source | Forme | Durée | Origine | +|---|---|---|---| +| `Breaker` | **par phase** | permanent | `phasePowerLimit`, réglage d'installation | +| `GridOperator` | **total** | temporaire, sur ordre | §14a — *le transport n'existe pas* | +| `EcoSelfImposed` | **total** | le temps de la passe 1 | §10 — *la passe n'existe pas* | + +> **Deux des trois sources n'ont pas encore d'émetteur, et c'est voulu.** Le mécanisme se conçoit +> pour trois parce que l'écrire pour une le rendrait irréversible : *« cela tient dans une +> signature aujourd'hui ; ce serait une refonte dans deux ans »* (LM-1006-1). + +--- + +## 3. Comment on compare sans convertir + +C'est le point technique du lot, et il n'est pas évident. On ne compare pas les **plafonds** — +ils n'ont pas la même forme. **On compare les MARGES qu'ils laissent.** + +``` +marge(cap) = combien de watts SUPPLÉMENTAIRES ce plafond autorise, ici et maintenant +``` + +- pour un plafond **par phase** : la marge est celle de la **phase la plus chargée**, ramenée en + watts — c'est déjà ce que fait `RootMeter::calculateAllowanceAmpere()` ; +- pour un plafond **total** : la marge est `totalW − soutirage mesuré`. + +La marge est un **nombre de watts** dans les deux cas : elle se compare. **Le plafond qui laisse +la plus petite marge MORD**, et c'est sa `source` qu'on retient. + +> **Ce que cela préserve** : aucune conversion, donc aucune hypothèse d'équilibre entre phases. +> Un plafond par phase reste évalué phase par phase jusqu'au bout ; il ne devient jamais un +> « total équivalent ». +> +> **Et ce que ça donne gratuitement** : la marge est exactement ce que la cascade doit connaître +> pour ne pas dépasser. Le même nombre sert à borner et à expliquer. + +--- + +## 4. Où le plafond mord — deux endroits, deux rôles + +Il faut les distinguer, sinon on écrit deux fois la même chose ou on en oublie une. + +| | Quand | Ce qu'il fait | +|---|---|---| +| **Dans la cascade** (L3, décision) | à chaque cycle, **avant** de commander | borne l'allocation : on ne décide pas d'acheter au-delà de la marge | +| **Dans la protection** (L4, réaction) | sur `powerBalanceChanged`, **après** la mesure | déleste ce qui dépasse déjà, dans l'**ordre inverse du rang** | + +**Les deux sont nécessaires, et ce n'est pas une redondance** : la première empêche de créer le +dépassement, la seconde rattrape ce qu'on n'a pas décidé — une plaque de cuisson, un +sèche-linge, tout ce que le HEMS ne pilote pas. La couche L4 garde le dernier mot +(`AGENTS.md`, MODÈLE DE SÉCURITÉ), et ce lot ne change pas cela. + +> **L'ordre de coupe est l'inverse du rang** : la charge servie en dernier est délestée en +> premier. C'est le seul ordre qui respecte le classement de l'installateur — l'inverse +> reviendrait à le contredire au moment où il compte le plus. + +--- + +## 5. Ordre de travail — quatre étapes, dont deux à comportement constant + +**Étape 1 — le TYPE et sa résolution.** `DrawCap`, la marge, « le plus contraignant l'emporte et +on sait lequel ». Testable **isolément** (`tests/auto/loadmodel`, sans serveur nymea). +**Comportement inchangé** : rien ne le consomme encore. + +**Étape 2 — la SOURCE `Breaker` branchée, à comportement CONSTANT.** `phasePowerLimit` devient un +`DrawCap{Breaker, perPhaseW}` et `evPhaseAllowanceW()` passe par la résolution. Le résultat doit +être **identique au watt près** — c'est ce qui rend l'étape vérifiable : les 46 tests de +`charging` ne doivent pas bouger d'une ligne. + +**Étape 3 — la cascade est bornée par la marge.** Le waterfall cesse d'allouer au-delà. C'est ici +que le comportement change pour les charges non-EV, et c'est ici que le motif `DRAW_CAP` naît +(**R8** : il publie sa source). + +**Étape 4 — la protection L4 déleste le waterfall.** `verifyOverloadProtection()` cesse d'être +réservée aux bornes ; ordre inverse du rang, `force = true`, verrous bypassés comme le repli L2. + +> **Les deux premières ne changent rien et rendent les deux suivantes petites.** C'est la même +> stratégie que le §10 : ce qui coûte, dans un lot de sécurité, n'est pas d'écrire le +> comportement — c'est de prouver qu'on n'a rien cassé en chemin. + +--- + +## 6. CE QUI DEMANDE UNE DÉCISION + +**6.1 — La marge doit-elle tenir compte de ce que le HEMS a lui-même commandé et qui n'est pas +encore mesuré ?** C'est la correction A, transposée au soutirage. Sans elle, deux cycles +consécutifs peuvent décider d'acheter la même marge deux fois. Avec elle, on réintroduit un +terme de latence dans une couche de **sécurité**, où une hypothèse fausse coupe du courant. +**Recommandation : NON dans la cascade, OUI nulle part ailleurs** — la cascade borne sur la marge +MESURÉE, et l'écart d'un cycle est absorbé par la marge que le disjoncteur garde par +construction (`allowancePerPhase -= 1` existe déjà côté proxy). À trancher explicitement. + +**6.2 — Que fait le délestage L4 d'une charge sous verrou `minOn` ?** Le repli L2 bypasse les +verrous (`force = true`) parce qu'un compteur muet est une panne. Un dépassement de plafond +est-il de la même gravité ? **Recommandation : OUI pour `Breaker` et `GridOperator`** — le +disjoncteur ne connaît pas le court-cycling, et un ordre du gestionnaire ne se négocie pas. +**NON pour `EcoSelfImposed`** : un plafond qu'on s'impose ne justifie pas d'abîmer un +compresseur. C'est la première conséquence concrète du fait que la source soit un paramètre. + +**6.3 — Le motif `DRAW_CAP` naît à l'étape 3 : que porte-t-il exactement ?** R8 demande la +source. Faut-il aussi `capW`, `marginW`, et la **phase** qui borne quand la source est +`Breaker` ? **Recommandation : oui aux trois** — sans la phase, un installateur devant une maison +triphasée déséquilibrée ne sait pas où aller mesurer. + +--- + +## 7. Ce que ce lot ne fait PAS + +- **Il ne crée aucun transport §14a.** `GridOperator` sera une source qu'aucun émetteur ne + remplit ; c'est le but — la porte reste ouverte, elle ne s'ouvre pas. +- **Il ne touche pas au repli L2.** LM-1006-1, propriété 2 : le plafond **auto-imposé** est + respecté par construction dès lors que le repli **ne dépense pas**. L'exigence dure — tenir + une borne imposée par un tiers alors que le moteur ne décide plus — appartient au **§14a** et + vivra **hors du moteur**. Les confondre ferait hériter à ce lot d'une contrainte qui ne le + concerne pas. +- **Il ne déplace pas la frontière de sécurité.** L4 garde le dernier mot, L0 reste physique.