Le vrai sujet n'était pas le banc mais le défaut d'usine. Une fois la règle batterie dans le budget, un seuil à 0,9 gèle toute l'installation sous 90 % de SOC — ni ECS, ni PAC, ni borne — sans erreur et sans panne. Personne ne s'en apercevait tant que la règle ne touchait que le VE. Le réglage confondait une CIBLE et une RÉSERVE. Une cible est ce qu'on aimerait atteindre, une réserve ce sous quoi on ne descend pas ; la règle « sous ce niveau, rien d'autre n'est servi » a une sémantique de réserve et était défaultée comme une cible. 0,2 protège l'autonomie du soir sans confisquer la journée, et un installateur qui veut « la batterie d'abord » monte la valeur délibérément — maintenant que l'écran l'expose. BATTERY_RESERVE publie le surplus RETENU, pas seulement le SOC : « 3 200 W retenus » se comprend là où « batterie à 47 % » se subit. LM-1203-b inscrit la nuance qui ne se déduit pas du code : sous la réserve le budget est annulé, pas réduit. Vérifié au passage, comme demandé : evReservedW devra filtrer sur funding == Grid après la seconde moitié — spot market et échéance restent au proxy, minCurrent et idle disparaissent. Il reste correct tel quel tant que le VE n'est pas dans la cascade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
10 KiB
DESIGN — 3g : la transplantation du VE dans le waterfall
Statut : conception, à valider avant tout code (AGENTS.md, workflow obligatoire).
Entrée : specs/spec_loadmodel.md §12, critère d'acceptation LM-1204-b.
Écrit après lecture de smartchargingmanager.cpp (1 900 lignes) et
rulebasedscheduler.cpp.
Ce document répond d'abord à la question posée : la transplantation perd-elle quelque chose de ce que le proxy fait de juste ? Oui, trois choses, et elles sont nommées au §3. Deux d'entre elles demandent une décision qui n'est pas technique.
1. Le constat qui justifie 3g, et il est plus dur que prévu
Le scénario des deux bornes n'est pas une faiblesse théorique du proxy. C'est un défaut
présent, et il se lit en huit lignes (planSurplusCharging, ~1215) :
double allowanceInWatt = -currentLoad;
double allowanceInAmpere = allowanceInWatt / 230;
while (it.hasNext()) {
EvCharger *evCharger = it.next();
double newRawValue = (evCharger->currentPower() / 230) + allowanceInAmpere;
…
if (chargingEnabled) it.remove();
}
allowanceInAmpere est calculé UNE fois, avant la boucle, et n'est jamais décrémenté.
Chaque borne reçoit donc la totalité du surplus disponible, comme si elle était seule.
Avec 4 kW et deux bornes, le proxy ne produit pas « 2 kW à chacune » — il produit « 4 kW à chacune ». Ce qui rattrape la situation n'est pas l'arbitrage : c'est le compteur au cycle suivant, puis la protection de surcharge. Le système converge par correction d'erreur, pas par décision.
Le it.remove() n'y change rien : il sort la borne de la liste pour les étapes ultérieures,
il ne rend pas son allocation au budget des suivantes.
Conséquence pour le critère LM-1204-b : le contre-exemple à ne pas produire — « 2 kW à chacune, rien ne charge » — n'est même pas le comportement actuel. Le comportement actuel est pire, et il est masqué par la boucle de rétroaction du compteur. Le test doit donc se lire ainsi : avant 3g, les deux bornes démarrent puis sont rattrapées ; après 3g, une seule démarre, délibérément.
2. Ce que la transplantation apporte sans rien ajouter
Ces points sont déjà résolus par le waterfall, et le VE en hérite gratuitement :
| Ce que le proxy fait à la main | Ce que le waterfall fait déjà |
|---|---|
| Allocation identique à toutes les bornes | Cascade : remainingSurplusW décrémenté à chaque charge |
| Rang des bornes inexistant | priority, trié une fois, un seul rang pour toutes les charges |
| Rendre à la borne sa propre consommation | Recrédit anti-clignotement (correction B), déjà en place |
EV_IDLE pour tout refus |
BELOW_MIN_POWER / SURPLUS_INSUFFICIENT, distinction du §12 |
Le plancher du VE existe déjà dans le proxy — minimalChargingCurrent × acquisitionTolerance
— et c'est exactement la notion du §12. Il devient declared.minPowerW.
⚠️ Avec une nuance qui compte : minimalChargingCurrent vaut
max(charger.maxChargingCurrentMinValue, car.minChargingCurrent) (ligne 605). Le plancher d'une
borne dépend donc de la voiture branchée. Ce n'est pas une valeur de configuration comme
pour l'ECS : c'est une valeur runtime, republiée à chaque descriptor(). Le §12 le permet
(le champ est dans LoadDeclared, pas seulement dans LoadConfig), mais l'écran ne doit pas la
présenter comme un réglage.
3. Ce qui NE se transporte PAS — les trois points à trancher
3.1 L'échéance : la machinerie n'a pas d'équivalent, et le waterfall ne sait pas acheter
Le proxy force la charge à fond quand projectedEndTime > cible − 10 min. Ce calcul repose sur
remainingPercentageToCharge = targetPercentage − carBatteryLevel (ligne 666) et sur une
projection de durée — de la machinerie propre au VE, alimentée par le SoC de la voiture.
needs { dailyDeadline, minEnergyWhPerDay } porte bien la déclaration d'une échéance. Mais :
- le scheduler ne la lit pas —
rulebasedscheduler.cppporte en toutes lettres « Grid funding (ECS/PAC) : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici » ; - forcer à fond pour tenir une échéance, c'est soutirer au réseau, donc franchir la même frontière que les planchers éco du §10.
Décision demandée. Trois options, et elles n'ont pas le même coût :
(a) 3g déplace l'allocation de surplus seulement, et l'échéance reste au proxy — qui continue de produire une action
TimeRequirementprioritaire, court-circuitant le waterfall quand elle mord. Le proxy ne disparaît pas ; il maigrit. (b) 3g embarque 3f (waterfall réseau) et l'échéance devient un besoin comme un autre. Lot bien plus gros, et il recoupe le §10. (c) L'échéance est suspendue en beta, la borne restant en surplus pur. À écarter : c'est la fonction qui a motivé le proxy.Ma recommandation : (a). Elle isole 3g sur ce qu'il corrige réellement — la cascade — et laisse l'échéance là où sa machinerie vit déjà. La fusionner avec 3f mêlerait deux franchissements de frontière dans un même lot.
3.2 La prise en compte de la batterie change de portée
Le proxy calcule, à l'intérieur du chemin VE : pause de la charge sous le seuil de SOC, tampon de 500 W quand la batterie charge sous le seuil, et réinjection de la puissance batterie dans le surplus au-dessus.
Ce n'est pas une règle « VE » : c'est une règle de budget. Transplanté tel quel, deux issues, et aucune n'est neutre :
- la monter dans le budget → l'ECS et la PAC se mettent, elles aussi, à respecter
batteryLevelConsideration. C'est probablement souhaitable — mais c'est un changement de comportement pour des charges qui ne l'ont jamais eu, sur des installations en service ; - la laisser au VE → il faut la réécrire dans l'adaptateur EV, et le budget publié en télémétrie cesse de correspondre à ce que le VE a réellement vu.
Décision demandée. Ma recommandation : la monter dans le budget, et le dire au changelog comme un changement de comportement assumé. Une batterie sous son seuil de réserve ne devrait pas plus alimenter un chauffe-eau qu'une voiture.
3.3 Le nombre de phases se décide en ampères, le waterfall parle en watts
getBestPhaseCount() choisit 1 ou 3 phases à partir du courant brut, puis divise. Le
waterfall, lui, alloue des watts — la conversion watts → ampères et le choix du nombre de
phases doivent donc vivre dans l'adaptateur EV, au moment d'exécuter.
Ce n'est pas une perte : c'est conforme à la règle 4 d'AGENTS.md (les bornes de l'adaptateur
écrêtent toute sortie de stratégie). Mais l'ordre change : aujourd'hui le nombre de phases
est choisi avant de connaître l'allocation finale ; demain il sera choisi après. Une borne
qui bascule en triphasé change son plancher effectif (6 A × 3 = 4,1 kW au lieu de 1,4 kW), donc
le plancher publié doit correspondre au nombre de phases visé, pas au nombre courant.
C'est le piège le plus subtil du lot, et il produirait un défaut silencieux : un plancher annoncé à 1,4 kW, une borne qui passe en triphasé, et une allocation de 2 kW qui ne démarre rien.
4. Ce qui reste au proxy, quoi qu'il arrive
planSpotMarketCharging— tarifs dynamiques (tier Predict AI), financementGrid, hors surplus. Précédence déjà gérée par le scheduler.verifyOverloadProtection/Recovery— protection par phase. Elle ne déleste que les bornes (§ du lot D) ; son extension au waterfall est une précondition du §10, pas de 3g.prepareInformation, l'estimation de SoC,setupPluggedInHandlers,lockOnUnplug— état du processus de charge, sans équivalent chez les autres charges.- Le filtrage d'entrée : borne sans voiture assignée, ou en
ChargingModeNormal. En waterfall, ces bornes doivent être absentes du contexte, pas présentes à rang 0 : une borne en mode manuel n'est pas « servie en dernier », elle est hors arbitrage.
4-bis. Vérifié : evReservedW après la transplantation
evReservedW (correction A) existe parce que l'EV est décidé avant le waterfall : sa
consigne du cycle n'est pas encore visible au compteur, donc on la réserve. Une fois le VE dans
la cascade, sa réservation devient son allocation — la compter en plus serait un double
comptage.
Audit du financement de chaque action EV (rulebasedscheduler.cpp) :
| action | financement | après 3g-1 |
|---|---|---|
| spot market (l. 61) | Grid |
reste au proxy → doit rester dans evReservedW |
buildTimeRequirementAction (l. 202) |
Grid |
reste au proxy → doit rester |
buildMinCurrentAction (l. 219) |
Surplus |
disparaît — remplacé par la cascade |
buildIdleAction (l. 233) |
Surplus |
disparaît — remplacé par les motifs du §12 |
Conclusion : le filtre de evReservedW doit devenir funding == LoadAction::Grid. Il ne
s'agit pas de le supprimer mais de le restreindre à ce qui reste hors cascade. Tant que la
seconde moitié de 3g-1 n'est pas faite, il reste correct tel quel.
5. Découpage proposé
| Étape | Contenu | Éprouvable |
|---|---|---|
| 3g-1 | EvAdapter publie declared.minPowerW (runtime, depuis la voiture) et LoadContext ; le VE entre dans nonEvLoads ; EV_IDLE cède à BELOW_MIN_POWER / SURPLUS_INSUFFICIENT |
oui — LM-1204-b, les deux bornes du banc |
| 3g-2 | Conversion watts → ampères + choix de phases dans l'adaptateur, plancher publié pour le nombre de phases visé | oui, banc |
| 3g-3 | batteryLevelConsideration monte dans le budget (§3.2) |
oui, banc |
| 3g-4 | Échéance : selon la décision du §3.1 | dépend |
3g-1 seul suffit à satisfaire LM-1204-b, et c'est le seul qui corrige le défaut du §1.
6. Ce qu'il faut décider avant que je code
- §3.1 — l'échéance : option (a), (b) ou (c) ? (Recommandation : (a).)
- §3.2 — la batterie : monter la règle dans le budget, en assumant le changement de comportement pour l'ECS et la PAC ? (Recommandation : oui.)
- Le découpage : 3g-1 livré et éprouvé seul, ou le lot entier d'un coup ? (Recommandation : 3g-1 seul — il porte la correction, et il est le seul dont le critère d'acceptation est déjà écrit.)