diff --git a/docs/DESIGN_DELESTAGE.md b/docs/DESIGN_DELESTAGE.md index a973594..7c3f352 100644 --- a/docs/DESIGN_DELESTAGE.md +++ b/docs/DESIGN_DELESTAGE.md @@ -299,3 +299,60 @@ la descente, mais `applyAction()` renvoyait l'action **demandée**, donc `state 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. + +## 8. Le délestage VISIBLE (lot du 2026-10-25) + +Question de Patrick : « Si le délestage coupe une charge, est-ce que le client voit une alerte +quelque part ? » Avant ce lot, non. Décision du 2026-10-22 : l'alerte passe par l'app, par les +événements JSON-RPC, sans aucun flux sortant (invariant 10). **Ce lot publie, il ne décide rien +de plus** : aucune consigne, aucun émetteur, aucun verrou ne change (invariant 2, règle 4). + +**8.1 — La borne : motif `OVERLOAD_THROTTLED`.** `verifyOverloadProtection()` rabaisse une borne +du dépassement arrondi à l'ampère supérieur, et la coupe sous son plancher. Ses consignes portent +`ChargingActionIssuerOverloadProtection`, mais rien n'en sortait. `executeChargingAction()` est +devenu virtuel (marque `[ETM]`, corps amont inchangé) : l'arbitre laisse partir la consigne, +puis la lit. + +| Consigne | Effet | +|---|---| +| protection, baisse ou coupure | la tenue s'ouvre ou se prolonge ; la borne publie `OVERLOAD_THROTTLED` | +| protection, remise au minimum après coupure (`verifyOverloadProtectionRecovery`) | la tenue se prolonge | +| protection, retour au courant manuel (`verifyOverloadProtectionRecovery`) | la tenue est levée | +| tout autre émetteur (le cycle reprend la borne) | la tenue est levée | +| émetteur inconnu (consigne par défaut de la boucle amont, voir 8.5) | rien | + +Paramètres : `fromA`, `toA` (0 si coupée), `cut`, `overshootW` (le plus fort dépassement de la +tenue), `limitA`, `phase` (absente pour une triphasée). La préséance est celle de L4 : l'entrée +perd ses niveaux, et `allocatedW` dit ce que la protection laisse à la borne. Hors d'un cycle +(`powerBalanceChanged`), l'instantané publié est complété et republié tel quel : son +`timestamp` reste celui du dernier cycle, et le geste est daté par le journal. + +**8.2 — Le journal court.** En mémoire, 20 épisodes (`SheddingLog`), lu par +`NymeaEnergy.GetSheddingLog`, même droit que `GetLoadTelemetry`. **Perdu au redémarrage de +nymead : accepté.** Il est hors de la télémétrie parce qu'il la doublerait (20 épisodes à deux +charges utiles de mécanisme, à chaque battement). La télémétrie n'en porte que le résumé, +`shedding: {revision, open}`, **absent** tant que rien n'a été délesté depuis le démarrage. + +- **Un épisode, pas un cycle.** Il s'ouvre au premier geste. Il se ferme à la fin du premier cycle + complet où la protection ne tient plus la charge, et la fin est datée du premier instant libre. +- **Toutes les charges de la cascade**, enregistrées dans `shedWaterfallForOverload()` sans liste + de types : « avant » et « après » sont la charge utile de `runtimeView()` (palier des relais, + consigne, état SG-Ready). Une charge de la cascade reste tenue tant que le plafond est épuisé au + compteur, ou que son motif est `DRAW_CAP`. +- « Après » garde l'état le plus bas de l'épisode, et `cut` dit si la charge a été coupée au moins + une fois. Une PAC rendue à l'état 2 n'est pas coupée. + +**8.3 — La notification.** `shedding.revision` change à chaque ouverture et à chaque fermeture. +`telemetrySignificantlyDiffers()` le compare : `LoadTelemetryChanged` part, même quand rien +d'autre n'a bougé. + +**8.4 — Limites connues.** Sous le mode dégradé L2, aucun cycle ne tourne : le journal ne se +ferme pas, il reprend au retour du compteur. Une charge que la cascade laisse éteinte parce que +la marge restante est sous son palier (`BELOW_MIN_POWER`) n'est plus tenue au sens du journal : +l'épisode se ferme quand le plafond n'est plus épuisé. + +**8.5 — Défaut amont relevé, NON corrigé.** Dans la boucle de `verifyOverloadProtection()`, une +borne monophasée active dont la phase ne dépasse pas reçoit `ChargingAction()` par défaut : +non enclenchée, 0 A, émetteur inconnu. Elle est donc coupée (verrous permettant) quand une autre +phase dépasse. Ce lot ne le publie pas comme geste de protection et ne le corrige pas : c'est +une décision de sécurité, hors de son périmètre.