# 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) : ```cpp 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.cpp` porte 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 `TimeRequirement` prioritaire, 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), financement `Grid`, 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 1. **§3.1 — l'échéance** : option (a), (b) ou (c) ? *(Recommandation : (a).)* 2. **§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.)* 3. **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.)*