diff --git a/docs/DESIGN_3g.md b/docs/DESIGN_3g.md new file mode 100644 index 0000000..8959969 --- /dev/null +++ b/docs/DESIGN_3g.md @@ -0,0 +1,172 @@ +# 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**. + +--- + +## 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.)*