setChargingInfo() appelle update() lui-même, et le lot le touche aux deux bouts. À LA POSE, l'ordre décide d'une minute. L'écriture du mode vient désormais en DERNIER, après l'insertion dans m_derogations : le cycle qu'elle déclenche trouve la dérogation déjà posée et la sert immédiatement. L'ordre inverse la lui faisait manquer d'un cycle — soixante secondes d'attente sur un mécanisme dont la définition est « tout de suite ». À LA FIN, la restitution est différée d'un tour de boucle. Les fins sont évaluées DEPUIS update() ; y appeler setChargingInfo() imbriquait un update() dans un update() — deux plans construits et deux dispatchs pour le même instant, sur des charges qu'un seul des deux croirait avoir commandées. update() n'a aucun verrou de ré-entrance : rien ne l'aurait signalé, et la trace aurait montré une borne commandée deux fois sans cause lisible. simulation 68/68, ci-quality 4/4, build 0 erreur 0 warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
319 lines
17 KiB
Markdown
319 lines
17 KiB
Markdown
# Dérogations — le boost n'est pas un mode
|
||
|
||
> Ouvert le 2026-09-07. **Design avant code, rien d'implémenté.**
|
||
> Cadrage donné par Patrick, et il vaut au-delà du VE.
|
||
|
||
## 1. La distinction qui structure tout le lot
|
||
|
||
| | mode | **dérogation** |
|
||
|---|---|---|
|
||
| se choisit | **une fois** | pour un **besoin ponctuel** |
|
||
| vaut | jusqu'au prochain choix | **jusqu'à ce que le besoin cesse** |
|
||
| se termine | jamais tout seul | **toujours tout seul** |
|
||
|
||
> **Un boost encore actif après la fin est un mode qu'on a oublié de quitter. Un boost qui rend
|
||
> la main est une dérogation terminée.**
|
||
|
||
C'est le même geste que forcer un chauffe-eau parce qu'on veut plus d'eau chaude : on ne
|
||
reconfigure pas l'installation, on demande quelque chose **maintenant**.
|
||
|
||
**Conséquence de conception** : ce n'est ni un `ChargingMode` de plus, ni une demande dans
|
||
`needs`. Une dérogation se **superpose** au mode courant et s'efface. Elle n'a ni cible d'énergie,
|
||
ni échéance, ni cadence — les trois supposent un besoin qui dure.
|
||
|
||
## 2. Ce qui la borne : la sûreté, et rien d'autre
|
||
|
||
Les trois comportements se distinguent par **ce qui les borne** :
|
||
|
||
| | borné par |
|
||
|---|---|
|
||
| PV | le **surplus** |
|
||
| Min+PV | un **plancher** + le surplus |
|
||
| **Boost** | la **sûreté seule** — `DRAW_CAP` et le délestage |
|
||
|
||
Et c'est cette dernière ligne qui le rend sûr : les plafonds deviennent la seule limite, **donc
|
||
une limite obligatoire**. Un boost sur un branchement 3×30 A sans `phasePowerLimit` ferait
|
||
disjoncter — ce n'est plus une hypothèse depuis le 2026-09-07, où l'on a découvert des semaines
|
||
de fonctionnement à `0`.
|
||
|
||
**Le boost prend la tête de la passe éco ; il ne surplombe pas la sûreté.** Si `DRAW_CAP` mord, il
|
||
est servi le premier **dans ce qui reste**, pas au-delà. Sans cette précision on retomberait sur
|
||
le second chemin d'achat refusé pour le minimum (`DESIGN_MIN_RESEAU.md` §3).
|
||
|
||
### Le rang
|
||
|
||
Le boost passe **devant**, y compris devant une charge mieux rangée. `LM-1002` dit que le rang est
|
||
ce que l'**installateur** décide ; elle ne dit rien d'une **action ponctuelle de l'utilisateur**.
|
||
Prendre la tête ne contredit donc pas la règle : cela passe par une porte qu'elle ne ferme pas.
|
||
Un boost servi après une autre charge contredirait « tout de suite », qui est sa définition.
|
||
|
||
## 3. Précaution 1 — le mode précédent se MÉMORISE, il ne se déduit pas
|
||
|
||
À la fin de la dérogation, la charge revient **au mode qu'elle avait**, relu depuis ce qui a été
|
||
stocké au déclenchement. Min+PV redevient Min+PV, PV redevient PV.
|
||
|
||
**Revenir à un défaut serait un défaut.** Retomber sur `Normal` sortirait la borne de l'arbitrage
|
||
— l'inverse exact du but, et une dérogation qui laisse l'installation moins pilotée qu'avant.
|
||
C'est la même famille que le repli d'échéance qui réécrivait `EcoWithMinCurrent` en `Eco` :
|
||
un état détruit en silence par un mécanisme qui croyait bien faire.
|
||
|
||
## 4. Précaution 2 — « terminé » se définit PAR MÉCANISME
|
||
|
||
Le moteur ne peut pas deviner ce qui met fin à une dérogation : cela dépend de ce qu'on déroge.
|
||
|
||
| charge | fins possibles |
|
||
|---|---|
|
||
| borne VE | **débranchement**, **cible en ÉNERGIE** (`sessionEnergy`), durée |
|
||
| chauffe-eau | **température** atteinte (si sonde), durée |
|
||
| batterie | cible en énergie, durée |
|
||
|
||
### La cible se dit en ÉNERGIE, jamais en SOC — et ce n'est pas un pis-aller
|
||
|
||
**Il n'y a pas de communication véhicule**, et il n'y en aura pas : aucun plugin constructeur
|
||
n'est prévu, et une intégration cloud Skoda/VW serait contraire au **100 % local**, fragile, et
|
||
rafraîchie à la minute au mieux.
|
||
|
||
Mais surtout, l'énergie est la **bonne** grandeur, indépendamment de cette contrainte :
|
||
|
||
| | ce qu'il faut savoir pour l'affirmer |
|
||
|---|---|
|
||
| « 20 kWh puis tu t'arrêtes » | `sessionEnergy`, que le plugin V2C construit depuis le 2026-08-29 |
|
||
| « jusqu'à 80 % » | la **capacité**, l'**état de santé** et le **rendement** de charge |
|
||
|
||
C'est exactement ce que `LM-1009` a établi pour l'obligation éco : **on mesure de l'énergie parce
|
||
que c'est ce qu'on sait mesurer.** Un SoC demanderait trois inconnues pour dire la même chose en
|
||
moins sûr.
|
||
|
||
> **La fin par SOC ne figure donc pas dans ce design**, et son absence n'est pas un manque à
|
||
> combler plus tard : c'est une décision. L'écrire comme un manque aurait laissé quelqu'un
|
||
> l'ouvrir comme un lot.
|
||
|
||
### Et `sessionEnergy` a un silence connu, qui justifie le filet
|
||
|
||
`LM-1012-d`, mesuré sur la V2C le 2026-08-30 : à la **perte du lien**, `currentPower` tombe à 0
|
||
et les booléens à `false`, mais **`sessionEnergy` reste à sa dernière valeur**.
|
||
|
||
Une dérogation « 20 kWh puis stop » dont le compteur se fige à 12 kWh **n'atteindra jamais sa
|
||
cible**. Elle achèterait au maximum jusqu'à ce que quelqu'un s'en aperçoive — sauf que la durée
|
||
est là. Ce n'est pas un cas d'école : c'est le comportement documenté de la borne installée.
|
||
|
||
**Chaque mécanisme déclare ce qui termine sa dérogation.** C'est un ajout à `ILoadAdapter`, au
|
||
même titre que `commandSettleS()` : une propriété du mécanisme, pas une constante du moteur.
|
||
|
||
## 5. LA RÈGLE — une dérogation qui ne sait pas dire quand elle finit ne devrait pas exister
|
||
|
||
C'est la règle centrale du lot, et elle se teste.
|
||
|
||
Sur un chauffe-eau **sans sonde**, « jusqu'à ce que ce soit chaud » n'est **pas mesurable**. La
|
||
fin doit alors être une **durée**, explicitement — et **l'app doit le dire à la saisie**, au
|
||
moment où l'utilisateur croit demander une température.
|
||
|
||
C'est exactement le raisonnement de `progressMeasurable` : une capacité absente se **dit** à la
|
||
saisie, parce que l'écran de résultat parle trop tard. Une dérogation dont la fin n'est pas
|
||
observable est une dérogation qui ne finira pas.
|
||
|
||
### La question qui manquait : que fait-elle ENTRE le figement et l'échéance ?
|
||
|
||
La durée arrêtera la dérogation — **au bout**. Entre-temps, elle achète au maximum sur une mesure
|
||
morte. Sur un lien qui perd 40 % de ses trames, « au bout » peut vouloir dire des heures.
|
||
|
||
**Position retenue : une dérogation dont la cible cesse d'être MESURABLE cesse.** Elle n'attend
|
||
pas sa durée.
|
||
|
||
C'est `R6` transposée. Là-bas : *servie oui, tenue jamais* — on ne conclut pas « tenue » sur un
|
||
avancement qu'on ne mesure pas. Ici : **active oui, vérifiable non, donc on s'arrête.** Une
|
||
dérogation est une commande qui **achète** ; la maintenir sans pouvoir dire où elle en est, c'est
|
||
acheter à l'aveugle.
|
||
|
||
**Et l'instrument existe déjà** : `measurement.source` et le régime de `LM-1009`. Quand le régime
|
||
d'une charge en dérogation cesse d'être mesuré, la condition de fin est perdue — et cela se sait
|
||
au cycle même, pas au bout de la durée.
|
||
|
||
### QUATRE fins, et le motif dit laquelle a mordu
|
||
|
||
| fin | ce qu'elle dit à l'utilisateur | geste |
|
||
|---|---|---|
|
||
| `targetReached` | la cible en énergie est atteinte | aucun — c'est le succès |
|
||
| `unplugged` | le véhicule est parti | aucun |
|
||
| **`measurementLost`** | on ne sait plus où en est la charge | **regarder le lien / le compteur** |
|
||
| `durationElapsed` | le temps imparti est écoulé | relancer si besoin |
|
||
|
||
Les quatre appellent des gestes différents, et une dérogation qui se terminerait sans dire
|
||
laquelle a mordu laisserait l'utilisateur relancer un boost que le lien empêchera encore.
|
||
|
||
### L'asymétrie, et la règle qu'elle donne
|
||
|
||
Le débranchement peut **clignoter** sur cette borne. Le plugin V2C a déjà la parade —
|
||
`k_absencesPourClore` polls consécutifs avant de clore une session — et elle porte une propriété
|
||
qu'il faut reprendre : **seuls les polls RÉUSSIS font avancer le compteur.** Une coupure réseau
|
||
le **gèle**, elle ne le fait pas progresser. Un lien mort ne peut donc pas fabriquer un faux
|
||
débranchement.
|
||
|
||
Cette confirmation vaut pour la dérogation aussi. Mais les deux erreurs ne coûtent pas la même
|
||
chose :
|
||
|
||
> **Terminer à tort** rend la main : la charge repasse à son mode mémorisé, et l'utilisateur
|
||
> relance s'il veut.
|
||
> **Continuer à tort** achète au maximum sur une mesure morte.
|
||
|
||
D'où la règle : **dans le doute, une dérogation s'arrête.** Les seuils de confirmation sont donc
|
||
asymétriques par conception — prompts à conclure la fin, lents à conclure la poursuite. C'est
|
||
l'inverse du réglage qu'on choisirait pour un mode, et c'est parce qu'une dérogation achète.
|
||
|
||
### Corollaire — la DURÉE est un filet, pas une option
|
||
|
||
Toute dérogation porte une durée maximale, **même quand elle a une meilleure condition de fin**.
|
||
|
||
Raison : la condition de fin repose sur un **instrument**, et un instrument peut se taire. Une
|
||
cible de charge qui ne remonte plus, une sonde qui tombe, un compteur qui se fige — et la
|
||
dérogation ne se termine jamais, en tirant le maximum. C'est « l'instrument dont le silence
|
||
ressemble à un résultat », appliqué à une commande qui achète.
|
||
|
||
**Donc : jamais « jusqu'à annulation ».** Quatre fins, et la durée est toujours l'une d'elles.
|
||
|
||
La durée est désormais **le filet du filet** : la perte de mesure attrape le cas où l'instrument
|
||
se tait ET se signale ; la durée attrape le cas où **même le régime ne se signale pas**. Deux
|
||
filets parce que le premier repose lui-même sur un instrument.
|
||
|
||
Cela rejoint ce que la maquette batterie demande pour son propre boost.
|
||
|
||
## 6. Ce que ça publie
|
||
|
||
Une dérogation active **se voit**, et sa fin **se lit** :
|
||
|
||
- un motif dédié, qui dit que la charge est en dérogation et **ce qui y mettra fin** ;
|
||
- l'instant de fin prévu, pour que l'écran affiche un compte à rebours plutôt qu'un état sans
|
||
horizon ;
|
||
- l'instant est **absolu, ancré à la capture** (`toUTC()`), jamais une heure murale — cf. la
|
||
dérive mesurée le 2026-09-06.
|
||
|
||
**Et la fin est un ÉVÉNEMENT**, publié le cycle où elle survient. Sans lui, l'écran ne peut pas
|
||
distinguer « la dérogation s'est terminée normalement » de « quelqu'un l'a annulée ».
|
||
|
||
## 7. Étapes, à comportement constant d'abord
|
||
|
||
**A** — le type et sa publication, sans qu'aucune dérogation ne puisse être posée. Le champ
|
||
existe, il ne décide de rien.
|
||
**B** — la déclaration des fins par mécanisme (`ILoadAdapter`), avec la durée obligatoire.
|
||
**C** — la pose, la mémorisation du mode précédent, et le retour automatique.
|
||
**D** — le service en tête de passe éco, borné par `DRAW_CAP` et le délestage.
|
||
|
||
Le pas **D** est le seul qui change ce qui est commandé, et il arrive quand tout le reste est
|
||
déjà vérifiable.
|
||
|
||
## 8. Ce que le banc ne prouvera pas
|
||
|
||
**Les trois fins du VE sont exerçables sans véhicule communicant** : débranchement, cible en
|
||
énergie via `sessionEnergy`, et durée. C'est ce qui rend ce lot éprouvable sur le banc réel, là
|
||
où une fin par SoC ne l'aurait jamais été.
|
||
|
||
Reste hors de portée : la fin par **température**, qui demande la sonde du §12 — elle n'existe
|
||
qu'en mock, et c'est le sixième manque de `INVENTAIRE_MOCK.md` sous un autre angle.
|
||
|
||
**La durée, elle, s'éprouve partout** — une raison de plus d'en faire le filet plutôt qu'un
|
||
recours.
|
||
|
||
---
|
||
|
||
## 9. Implémenté — `+etm59`, 2026-09-08
|
||
|
||
Le design ci-dessus est posé **d'un bloc** et non par les quatre pas du §7 : la fenêtre de peu de
|
||
production du matin était le bon moment pour éprouver un mécanisme qui **achète**, et découper
|
||
l'implémentation aurait fait passer cette fenêtre.
|
||
|
||
### Où vit quoi
|
||
|
||
| | fichier |
|
||
|---|---|
|
||
| le type, les quatre fins, les seuils | `etm/types/derogation.h` |
|
||
| pose, levée, évaluation des fins, restitution du mode | `etm/energyarbitrator.cpp` |
|
||
| service en **tête de passe éco** | `etm/scheduler/rulebasedscheduler.cpp` |
|
||
| `SetLoadDerogation` / `ClearLoadDerogation`, `derogationEnded` | `nymeaenergyjsonhandler.cpp` |
|
||
|
||
### Trois décisions prises en écrivant, qui ne sont pas dans le design
|
||
|
||
**a. Les fins se lisent dans le CONTEXTE DU CYCLE, pas dans les adaptateurs.**
|
||
`evaluerDerogations()` tourne **entre** `buildContext()` et `getPlan()`, sur les `LoadContext`
|
||
que le scheduler va lire. Une seconde lecture donnerait une seconde vérité, et une dérogation
|
||
pourrait se terminer sur une mesure que le plan ne voit pas. C'est aussi ce qui rend le
|
||
débranchement gratuit : une borne débranchée est déjà filtrée du contexte par
|
||
`evParticipatesInWaterfall()`, son absence **est** le signal.
|
||
|
||
**b. `measurementLost` ne mord QUE s'il y a une cible.**
|
||
Le design disait « une dérogation dont la cible cesse d'être mesurable doit cesser ». Sans cible,
|
||
rien ne dépend de la mesure : le filet de durée suffit, et couper là punirait la charge du
|
||
silence d'un instrument dont elle n'a pas besoin. La garde porte donc sur `targetWh > 0`.
|
||
|
||
C'est aussi pourquoi le critère est `telemetry.sessionMeasurable` et **non** `progress.regime` :
|
||
`progress` est absent sur une charge sans obligation en énergie — c'est-à-dire exactement le cas
|
||
du boost, où le critère aurait été inerte.
|
||
|
||
**c. La base de la cible se capture au PREMIER CYCLE, pas à la pose.**
|
||
Au moment où l'utilisateur appuie, aucun cycle n'a construit de contexte et le cumul de session
|
||
n'est lisible nulle part. Prendre `0` compterait depuis le début de la **session de charge** :
|
||
une dérogation « 5 kWh » posée sur une session déjà à 6 kWh serait atteinte avant d'avoir
|
||
commencé. Mécanisme de `LM-1012-b`, même raison — une base se prend sur ce qu'on mesure.
|
||
|
||
### Le seul endroit où le mode est ÉCRIT
|
||
|
||
Une borne en `Normal` (manuel) est **hors waterfall** : le moteur ne la commande pas, donc une
|
||
dérogation n'y produirait rien du tout. La pose la bascule alors en éco pour la durée, et
|
||
`modeForce` retient qu'elle l'a fait. C'est le seul cas — et c'est lui qui donne sa raison d'être
|
||
à `previousMode` : sans mémorisation, on rendrait une borne manuelle en éco, c'est-à-dire qu'on
|
||
lui prendrait un réglage en silence.
|
||
|
||
### La passe confort est SAUTÉE sous dérogation
|
||
|
||
La fusion des niveaux est une **addition** (`la.powerW += eco.targetW`). La dérogation demande
|
||
déjà le maximum de la charge : y ajouter du surplus dépasserait son plafond matériel. Le surplus
|
||
non dépensé reste au budget et passe aux charges suivantes.
|
||
|
||
**Conséquence comptable à connaître** : la dérogation est comptée **entièrement en achat** alors
|
||
que le surplus présent la finance en partie, physiquement — `gridW` la surestime dès qu'il y a du
|
||
soleil. Ce n'est pas une approximation neuve : le §10 la porte déjà pour le plancher éco, et elle
|
||
sera reprise avec lui. C'est aussi pourquoi la première vérification au banc se fait le matin.
|
||
|
||
### Ce que le test prouve, et par quelle contre-épreuve
|
||
|
||
`Simulation::testDerogationTakesTheHeadButNotTheSafety` — deux charges, la borne au **rang 9**
|
||
(la queue) et l'ECS au **rang 1**, sans aucun surplus :
|
||
|
||
1. **contre-épreuve** — la même borne **sans** dérogation reçoit `0 W`, l'ECS ses 1 000 W de
|
||
cadence. Sans elle, on ne saurait pas d'où viennent les 7 000 W du cas suivant ;
|
||
2. avec dérogation : 7 000 W, `funding = Grid`, motif `DEROGATION_ACTIVE`, `endsAt` publié, et
|
||
**pas** de `targetWh` puisqu'il n'y en a pas ;
|
||
3. **la tête de passe prouvée par la rareté** — 7 500 W d'autorisation pour 8 000 demandés : la
|
||
borne de rang 9 est servie **entièrement**, l'ECS de rang 1 prend les 500 restants. L'inverse
|
||
exact de l'ordre des rangs, donc la preuve que c'est bien la tête de passe qui agit ;
|
||
4. 1 000 W d'autorisation : servie à 1 000, `boundBy: drawCap` — elle ne surplombe pas la sûreté ;
|
||
5. autorisation nulle : **aucun niveau**, pas un niveau à zéro.
|
||
|
||
### Ce qui reste ouvert
|
||
|
||
- La **fin par cible** n'est pas éprouvée au banc : elle demande une session assez longue pour
|
||
franchir un seuil, et la V2C a un `sessionEnergy` qui **se fige** à la perte du lien
|
||
(`LM-1012-d`) — c'est précisément ce que le filet de durée couvre.
|
||
- `ClearLoadDerogation` publie la cause `cancelled`, qui n'est pas une des quatre fins par
|
||
mécanisme : c'est une fin **par décision**, et la distinguer est le but.
|
||
|
||
### Deux effets de `setChargingInfo()` trouvés en préparant l'essai — `+etm60`
|
||
|
||
`SmartChargingManager::setChargingInfo()` **déclenche lui-même un `update()`**. Écrire le mode
|
||
n'est donc pas un geste neutre, et le lot le touche aux deux bouts.
|
||
|
||
**À la pose, l'ordre décide d'une minute.** L'écriture du mode vient **en dernier**, après
|
||
l'insertion dans `m_derogations` : le cycle qu'elle déclenche trouve alors la dérogation déjà
|
||
posée et la sert immédiatement. L'ordre inverse la lui faisait manquer d'un cycle — soixante
|
||
secondes d'attente sur un mécanisme dont la définition est « tout de suite ».
|
||
|
||
**À la fin, la restitution est DIFFÉRÉE d'un tour de boucle.** Les fins sont évaluées *depuis*
|
||
`update()` ; y appeler `setChargingInfo()` imbriquerait un `update()` dans un `update()` — deux
|
||
plans construits et deux dispatchs pour le même instant, sur des charges qu'un seul des deux
|
||
croirait avoir commandées. `update()` n'a aucun verrou de ré-entrance, donc rien ne l'aurait
|
||
signalé. Le décalage ne se voit nulle part : l'événement de fin part dans la trame du cycle
|
||
courant, et le mode est rendu avant le suivant.
|
||
|
||
**Effet heureux du retour à `Normal`** : l'amont restaure alors les valeurs manuelles
|
||
(`manualChargingEnabled`, `manualMaxChargingCurrent`, `manualDesiredPhaseCount`). La borne
|
||
retrouve donc *ses* réglages, et pas seulement son mode.
|