docs(design): §10 ouvert — éco/confort avec le délestage, et la prépa du banc V2C

Deux livrables, aucun code.

DESIGN_10.md — le §10 sous la contrainte des deux notes.

Le délestage part AVEC pour une raison arithmétique, pas de calendrier : la
passe éco n'est pas bornée par le surplus mais par une autorisation de
soutirage, et cet objet n'existe pas encore. Le lot doit donc le créer.

LM-1006-1 devient une structure : DrawCap {source, totalW, perPhaseA, until} et
DrawBudget qui publie `binding` — QUI a mordu. Le piège technique est nommé et
tranché : les trois sources ne parlent pas la même langue (le disjoncteur est
par phase, le §14a et l'éco sont des totaux), un plafond total ne protège pas
d'une surcharge monophasée, un plafond par phase n'exprime pas le §14a. La
proposition est de porter les deux et de ne JAMAIS convertir — c'est la
conversion qui perdrait l'information.

LM-1003 impose la vraie refonte : les passes calculent des cibles, une seule
commande sort par charge et par cycle, arrondie une fois au mécanisme. Cela
scinde buildSetpointAction(), qui mélange aujourd'hui décision, écrêtage,
comptabilité et motif. Deux comptabilités distinctes, PlanBudget (d'où vient
l'énergie) et PlanDraw (combien on a décidé d'acheter) — les fondre rendrait
faux l'invariant publié de la première.

Cinq points à trancher, dont trois ne sont pas techniques. Le premier est le
plus lourd : la réserve batterie annule le budget de SURPLUS, mais la passe éco
dépense une autorisation de SOUTIRAGE. Si la réserve l'annule aussi, un plancher
éco ne sera jamais atteint batterie basse ; si elle ne l'annule pas, la maison
achète pendant qu'elle manque. Recommandation donnée, décision d'exploitation.

L'ordre de travail met en tête les deux étapes qui NE CHANGENT RIEN — publier le
plafond sans effet, puis la refonte décider/émettre à comportement constant,
prouvée par l'invariance des 16 scénarios enregistrés. Si un seul point de
contrôle bouge, c'est qu'une décision a changé, et il faut savoir laquelle.

PREPA_BANC_20260828.md — deux blocages silencieux trouvés la veille.

1. La ThingClass trydan ne déclare PAS phaseCount, et aucun registre Modbus V2C
   ne le porte. EvCharger::phaseCount() retombe sur 1 : sur une Trydan
   triphasée, le plancher publié vaudrait 1 380 W au lieu de 4 140, et la
   conversion W→A donnerait une consigne 3× trop haute. C'est exactement la
   famille de défaut que 3g-2 vient de fermer, rouverte côté plugin. Correctif
   proposé : saisie à l'appairage, comme la voiture simulée le fait déjà.
2. sessionEnergy n'est délibérément pas exposée — le brief du plugin l'interdit,
   evcc ayant retiré son ChargeRater pour cette borne (firmware peu fiable).
   Donc « sessionEnergy sur matériel réel » n'est PAS franchissable demain. En
   échange, demain permet la validation qui lèverait l'interdit, et exerce pour
   de vrai la condition C1 de LM-1009 sur du matériel.

Plus : le plugin V2C n'est pas installé sur le banc, la Skoda n'a aucune
intégration (donc voiture manuelle, avec les quatre réglages à saisir), et
l'IP de test .127 est libre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
This commit is contained in:
Patrick Schurig 2026-08-27 15:06:37 +02:00
parent d4ad01324f
commit 16feb61149
3 changed files with 491 additions and 2 deletions

View File

@ -193,8 +193,13 @@ renuméroter les autres, ou assumer l'égalité et laisser l'app poser les vrais
en service. Sans conséquence sur la reproductibilité (le tri est total), donc à décider, pas à
réparer dans l'urgence.
Le **§10** attend une session propre. Deux notes de conception l'y attendent, à lire avant de
coder : **LM-1006-1** (le délestage est un plafond de soutirage à trois sources) et **LM-1009**
**§10 — OUVERT.** Le design est écrit : **`docs/DESIGN_10.md`**, à valider avant tout code. Il
porte l'ordre de travail en cinq étapes (dont deux à comportement constant, qui rendent le reste
petit) et **cinq points à trancher**, dont trois ne sont pas techniques — le premier étant : la
réserve batterie annule-t-elle aussi la passe éco ?
Deux notes de conception le contraignent, à relire avant de coder :
**LM-1006-1** (le délestage est un plafond de soutirage à trois sources) et **LM-1009**
(une échéance ne vaut que ce que son avancement se mesure — `sessionEnergy` est publiée et lue
par personne, tandis que le pourcentage comparé à la cible est estimé par le moteur lui-même).
LM-1009 porte **trois décisions arrêtées** — avancement en énergie livrée, obligation par session
@ -762,6 +767,11 @@ supérieures n'affecte pas les couches inférieures. Voir `docs/SAFETY.md` pour
- `docs/RELEVE_3g1.md` — relevé MACHINE de 3g-1 sur `.75` : ce que le banc a dit, y compris les
trois défauts qu'il a révélés et que 3g-2 corrige.
- `docs/RECAP_3g2.md` — 3g-2 : ce que le lot corrige, et pourquoi.
- `docs/DESIGN_10.md` — design du §10 (éco/confort + délestage), **à valider**. Le plafond de
soutirage devient un objet, les deux passes calculent des cibles et une seule commande sort.
- `docs/PREPA_BANC_20260828.md` — préparation du banc pour la V2C Trydan et la Skoda Enyaq, avec
**les deux blocages silencieux** trouvés la veille (`phaseCount` absent, `sessionEnergy`
délibérément non exposée).
- `docs/BRIEF_depuis_plugin.md` — **brief ÉMIS vers l'agent app** après 3g-2 : ce qui bouge pour
l'écran (l'inclusion rétablie, la mise en garde sur `allocatedW` qui tombe, `EV_GRID_START`,
le rang de borne au glisser-déposer).

327
docs/DESIGN_10.md Normal file
View File

@ -0,0 +1,327 @@
# DESIGN — §10 : éco / confort, et le délestage dans le même lot
**Statut : conception, à valider avant tout code** (`AGENTS.md`, workflow obligatoire).
**Entrée** : `specs/spec_loadmodel.md` §10 (LM-1000 → LM-1009) et §12.
**Écrit après relecture de** `rulebasedscheduler.cpp` (`getPlan`, `buildSetpointAction`,
`buildSgReadyStateAction`), `smartchargingmanager.cpp` (`verifyOverloadProtection`), et des deux
notes qui contraignent la forme : **LM-1006-1** (le plafond de soutirage) et **LM-1009** (les
deux régimes d'échéance).
> **Ce document ne demande pas si le §10 doit se faire. Il dit sous quelle forme, et nomme les
> cinq points qui demandent une décision** — §8. Trois d'entre eux ne sont pas techniques.
---
## 1. Pourquoi le délestage part AVEC, et pas après
C'est la précondition écrite en §13 de la spec, et elle tient pour une raison arithmétique
simple : **la passe éco n'est pas bornée par le surplus** (LM-1005). Elle est bornée par une
autorisation de soutirage. Or aujourd'hui, la seule borne de soutirage qui existe dans le moteur
— `phasePowerLimit` — ne déleste **que les bornes de recharge**
(`verifyOverloadProtection`, boucle sur `m_evChargers`).
Livrer les planchers éco sans le délestage général donnerait donc : un matin sans soleil, chaque
charge se sert au réseau pour tenir son obligation, personne ne les additionne, et
l'installation disjoncte. Ce n'est pas une dégradation de confort — c'est une coupure, chez un
client.
**Conséquence de conception, pas de calendrier** : la passe éco a besoin d'un objet « plafond »
qui n'existe pas encore. Le §10 doit donc le créer. C'est le §2.
---
## 2. Le plafond de soutirage devient un OBJET (LM-1006-1)
### 2.1 Ce qu'il remplace
Aujourd'hui, la limite de raccordement est consultée à deux endroits, sous deux formes, sans
jamais être nommée :
- `RootMeter::calculateAllowanceAmpere(phases, phasePowerLimit)` — la place libre **par phase**,
interrogée par le proxy puis, depuis 3g-2, par `EnergyArbitrator::evPhaseAllowanceW()` ;
- `verifyOverloadProtection()` — la réaction temps réel, qui déleste les bornes.
Aucun des deux ne se demande *d'où vient* le plafond, parce qu'il n'y en a qu'un. Le §14a en
ajoutera un second, le §10 lui-même un troisième.
### 2.2 La forme proposée
```cpp
/*! Un plafond de soutirage, et ce qui l'a produit. */
struct DrawCap {
enum Source { Connection, GridOperator, EcoSelfImposed };
Source source = Connection;
double totalW = -1; //!< Plafond au POINT DE LIVRAISON. < 0 = pas de plafond total.
QList<double> perPhaseA; //!< Plafond PAR PHASE. Vide = pas de plafond par phase.
QDateTime until; //!< Fin de validité ; invalide = permanent.
};
/*! Le plafond EFFECTIF du cycle, et celui qui a mordu. */
struct DrawBudget {
QList<DrawCap> declared; //!< Tous les plafonds actifs, quelle que soit leur source.
double effectiveW = -1; //!< Le plus contraignant, ramené en watts.
DrawCap::Source binding = DrawCap::Connection; //!< QUI a mordu — publié, pas déduit.
bool perPhaseBound = false; //!< Vrai si c'est une phase qui borne, pas le total.
};
```
**`binding` est le champ qui justifie tout l'objet.** Sans lui, un client bridé lit « protection
de surcharge » là où la cause est le gestionnaire de réseau — deux causes, deux gestes
(LM-1006-1, propriété 1).
### 2.3 Le piège : par phase, ou au total ?
Les trois sources ne parlent pas la même langue, et c'est le seul vrai problème technique du §2.
| Source | Forme naturelle | Peut-on la convertir ? |
|---|---|---|
| Disjoncteur de branchement | **par phase** (A) | pas en un total : 3 × 32 A ne vaut pas 22 kW répartissables |
| §14a EnWG | **total** (W au point de livraison) | pas en par-phase : le gestionnaire ne dit rien de la répartition |
| Plancher éco (§10) | **total** (W qu'on s'autorise) | idem |
**Un plafond total ne protège pas d'une surcharge monophasée** : 4 kW tirés entièrement sur L1
passent un plafond total de 6 kW et font disjoncter un 16 A. **Et un plafond par phase ne sait
pas exprimer le §14a.**
**Décision de conception proposée** : `DrawBudget` porte **les deux**, les évalue **toutes les
deux**, et `binding` dit laquelle a mordu la première. Aucune conversion, jamais — c'est la
conversion qui perdrait l'information. `effectiveW` n'est qu'une commodité d'affichage, dérivée,
et `perPhaseBound` dit s'il faut la lire avec précaution.
### 2.4 Où il se calcule
Une fois par cycle, dans `buildContext()`, à côté de `ctx.meter` et `ctx.battery` — c'est déjà
l'endroit qui lit le compteur et connaît `phasePowerLimit`. Il devient un champ de
`SurplusContext` :
```cpp
struct SurplusContext {
// … existant …
DrawBudget drawCap; //!< [§10] Plafond de soutirage du cycle, toutes sources confondues.
};
```
Publié en télémétrie au même titre que `budget` — c'est la contrepartie de `binding` : un client
qui ne voit pas le plafond ne peut pas expliquer ce qu'il affiche.
---
## 3. Les deux passes, et ce qui borne chacune
`LM-1002` fixe la structure : deux passes sur la **même** liste, triée une seule fois par rang.
Ce qui change entre elles n'est pas l'ordre, c'est **la borne**.
```
┌────────────── passe 1 : ÉCO ──────────────┐
liste triée par │ borne : DrawBudget (autorisation de │
rang (une seule) │ SOUTIRAGE — pas le surplus) │
rang 1 ─────────▶│ sert : le plancher `needs` non atteint │
rang 2 ─────────▶│ produit : une CIBLE, pas une commande │
rang 3 ─────────▶└───────────────────────────────────────────┘
│
│ ┌──────────── passe 2 : CONFORT ────────────┐
└──────────▶│ borne : le RELIQUAT de surplus │
(même ordre) │ sert : la cible de confort si demandeuse │
│ produit : une CIBLE, pas une commande │
└───────────────────────────────────────────┘
│
somme par charge, arrondie UNE fois
│
UNE commande
```
**Ce que la passe 1 dépense n'est pas ce que la passe 2 dépense.** C'est le point que le code
actuel ne sait pas exprimer : `getPlan()` n'a qu'un compteur, `remainingSurplusW`, et
`buildSetpointAction()` le décrémente **et** émet l'action dans le même geste
(`rulebasedscheduler.cpp:207-212`). Le §10 doit séparer les deux — voir §4.
**Deux comptabilités, donc, et elles ne se mélangent pas** :
```cpp
struct PlanBudget { // existant, inchangé dans sa sémantique
double surplusW, evReservedW, allocatedW, recreditedW, remainingW;
};
struct PlanDraw { // [§10] ce que la passe 1 a décidé d'ACHETER
double authorisedW = 0; //!< Ce que le DrawBudget laissait prendre.
double committedW = 0; //!< Ce que la passe 1 a réellement engagé au réseau.
double remainingW = 0; //!< Reliquat d'autorisation, non dépensé.
};
```
> **Pourquoi deux structures et pas un champ de plus** : `PlanBudget` répond « d'où vient
> l'énergie servie » ; `PlanDraw` répond « combien a-t-on décidé d'acheter ». Les fondre
> rendrait l'invariant publié de `PlanBudget` (`surplus + recrédit − alloué == restant`) faux
> ou incompréhensible, et c'est lui qui rend la charge utile vérifiable par un client.
---
## 4. Une seule commande par charge et par cycle (LM-1003)
C'est la contrainte qui impose la refonte structurelle, et elle est petite à énoncer :
**les passes calculent, elles ne commandent pas.**
Découpage proposé de `getPlan()` :
```cpp
// 1. contexte, budget de surplus, plafond de soutirage (inchangé + DrawBudget)
// 2. tri UNE fois (inchangé)
QHash<QString, LoadTarget> cibles; // loadId → { ecoW, confortW, motifs… }
// 3. passe 1 — éco, bornée par ctx.drawCap
for (const LoadContext &lc : chargesTriees)
cibles[lc.id].eco = planifierEco(lc, drawRestant, planDraw);
// 4. passe 2 — confort, bornée par le reliquat de surplus
for (const LoadContext &lc : chargesTriees)
cibles[lc.id].confort = planifierConfort(lc, surplusRestant, planBudget);
// 5. UNE émission par charge : somme, arrondi au mécanisme, motif
for (const LoadContext &lc : chargesTriees)
slot.actions.append(emettre(lc, cibles.value(lc.id)));
```
**L'arrondi au mécanisme se fait à l'étape 5, une seule fois.** C'est ce qui empêche le défaut
que LM-1003 nomme : un relais commuté en passe 1 puis recommuté en passe 2, soit du
court-cycling fabriqué par l'arbitre lui-même.
**Le recrédit s'attribue à la première passe où la charge apparaît** (LM-1004) — donc dans
`planifierEco()` si elle a un plancher, dans `planifierConfort()` sinon. Un champ
`recreditAttribue` dans `LoadTarget` le rend vérifiable plutôt que discipliné.
> **Ce que cette refonte casse, et qu'il faut prévoir** : `buildSetpointAction()` et
> `buildSgReadyStateAction()` mélangent aujourd'hui décision, écrêtage de verrou, comptabilité
> et motif. Ils doivent se scinder en « décider une cible » et « émettre ». C'est le gros du
> travail du lot, et c'est du remaniement, pas de l'ajout. Les tests de motif existants
> (`testDecisionReasonRendering`, `testEcsBudgetUnderLock`, `testEcsSurplusPV`) sont le filet :
> ils portent sur le TEXTE publié, donc ils survivront au remaniement ou le condamneront.
---
## 5. Le délestage : ce qu'il coupe, et dans quel ordre
**Le plafond borne le PLAN, il ne le corrige pas après coup.** C'est la leçon de 3g-2 : un
plafond appliqué après l'allocation laisse le waterfall croire qu'il a servi.
- **En passe 1**, chaque charge servie décrémente `planDraw.remainingW`. Quand l'autorisation est
épuisée, les charges suivantes reçoivent 0 **et le motif nomme la source** (§6).
- **L'ordre de délestage est l'inverse de l'ordre de service** : le rang décide qui est servi en
premier, donc qui est coupé en dernier. Aucun second classement — c'est LM-1002 appliqué à
l'envers, et c'est la seule lecture qui ne surprendra pas l'installateur.
- **L4 ne bouge pas.** `verifyOverloadProtection()` garde le dernier mot en temps réel, sur la
mesure, après le dispatch. Le §10 lui ajoute des charges à délester ; il ne lui retire pas son
rôle et ne s'y substitue pas. **Sa position dans `update()` reste INTOUCHABLE** (AGENTS,
modèle de sécurité).
---
## 6. Les motifs — ce que le §10 ajoute au catalogue
Le catalogue est fermé et testé sur le texte ; toute clé ajoutée demande une ligne de rendu et
une entrée de clôture (`testDecisionReasonRendering`).
| Code | params | Quand |
|---|---|---|
| `DRAW_CAP` | `capW`, `source`, `requiredW`, `o:phase` | Le plafond de soutirage refuse, et **`source` dit lequel** : raccordement, gestionnaire de réseau, ou auto-imposé. `phase` présent si c'est une phase qui borne. |
| `ECO_FLOOR_GRID` | `deliveredWh`, `targetWh`, `gridW` | Une obligation éco servie **au réseau**. `gridW` chiffre l'achat, comme `EV_GRID_START` le fait déjà. |
| `ECO_FLOOR_MET` | `deliveredWh`, `targetWh` | L'obligation est **atteinte** — et elle ne peut se dire atteinte que sur le régime ÉNERGIE (LM-1009). |
| `COMFORT_SKIPPED` | `o:sensorValue`, `o:setpoint` | La charge n'est plus demandeuse de son bonus (comparateur de LM-1008). Distinct de « pas de budget ». |
**Et un champ, pas un code** : le **régime** de l'avancement (mesuré / estimé / non mesurable),
publié sur chaque charge portant une obligation. C'est la condition C2 de LM-1009, et elle ne
peut pas vivre dans un code : les trois régimes coexistent dans le même cycle, sur des charges
différentes.
---
## 7. Ce que LM-1009 impose à l'obligation éco
Trois décisions déjà prises, rappelées ici parce qu'elles ferment des choix de conception :
- **D1** — l'avancement se mesure et s'affiche en **énergie livrée**. La passe 1 compare donc des
Wh à des Wh, jamais un pourcentage.
- **D2** — **obligation par SESSION** d'abord. Pour une borne, la session est le branchement, et
la source est `sessionEnergy`. Pour une charge thermique, la « session » est le cycle
quotidien — et c'est là que l'analogie s'arrête : `minEnergyWhPerDay` **reste hors de ce lot**,
faute d'accumulation inter-sessions (§11).
- **C1** — une charge dont l'énergie livrée **n'est pas mesurable** dégrade **visiblement**. Le
cas est réel dès demain : la V2C Trydan ne publie pas `sessionEnergy`
(`docs/PREPA_BANC_20260828.md` §2.2). Le §10 doit donc prévoir ce régime **dès l'écriture**,
pas comme un cas dégradé ajouté après.
> **Conséquence directe** : une obligation éco sur une charge non mesurable est **déclarative,
> pas vérifiable**. Elle peut être servie ; elle ne peut jamais être annoncée **tenue**. C'est
> la même frontière que LM-1009 trace pour l'échéance VE.
---
## 8. LES CINQ POINTS À TRANCHER
**8.1 — La réserve batterie annule-t-elle aussi la passe ÉCO ?** *(le plus important)*
LM-1203-b dit : sous la réserve, le budget est **annulé**. Mais ce budget est un budget de
**surplus**, et la passe 1 ne dépense pas de surplus — elle dépense une autorisation de
soutirage.
- **Si la réserve annule aussi la passe 1** : une installation dont la batterie est basse
n'atteindra **jamais** son plancher éco. Un ballon qui n'a pas vu 50 °C ne les verra pas, parce
que la batterie de la maison est à 15 %.
- **Si elle ne l'annule pas** : la maison achète au réseau pour un chauffe-eau pendant que sa
batterie est basse — ce qui peut être exactement ce qu'on veut (l'obligation prime) ou une
absurdité (on achète pendant qu'on manque).
**Ma recommandation : la réserve N'annule PAS la passe 1.** La réserve protège le **surplus** ;
une obligation d'énergie sur échéance est d'une autre nature, et si elle cède à la réserve, elle
cesse d'être une obligation. Mais c'est une décision d'exploitation, pas de code.
**8.2 — Le plafond §14a est-il par phase, au total, ou les deux ?** Le §2.3 propose de porter les
deux sans jamais convertir. Reste à confirmer que c'est bien ainsi que le gestionnaire de réseau
l'exprimera — et le transport du §14a n'existe pas encore, donc la réponse peut attendre à
condition que la **forme** ne se ferme pas maintenant.
**8.3 — Le repli L2 doit-il TENIR le plafond ?** LM-1006-1, propriété 2 : le plafond survit à la
panne du HEMS, et ce sera une obligation légale pour le §14a. Or le repli L2 actuel est conçu
pour **ne rien initier** (`SAFETY.md` §L2), pas pour continuer à faire respecter une borne
imposée par un tiers. Les deux ne sont pas la même chose. **Ma recommandation** : en mode
dégradé, le plan cesse, mais une allocation **statique** respectant le dernier plafond connu est
maintenue. C'est un changement de nature du repli, et il mérite d'être décidé, pas déduit.
**8.4 — La cible de confort : comparateur seul, ou dosage ?** LM-1008 tranche déjà : comparateur
(`sonde < consigne` → demandeuse), le dosage attend le modèle thermique du §11. **À confirmer
que ça reste vrai pour ce lot**, parce que c'est ce qui le rend faisable sans historique.
**8.5 — Que fait l'app des deux passes ?** LM-1006, couplage 2 : un plancher éco de rang 4 passe
devant un confort de rang 1, et c'est **contre-intuitif** pour l'installateur qui vient de poser
son ordre. Sans écran qui montre les deux passes, le premier retour terrain sera « l'ordre n'est
pas respecté ». Ce n'est pas du travail moteur, mais c'est une condition de livraison du lot.
---
## 9. Ce qui n'est PAS dans ce lot
- **`minEnergyWhPerDay`** — obligation quotidienne, exige l'accumulation inter-sessions (§11).
Le lot livre l'obligation **par session**.
- **Le transport du §14a** — aucun canal n'existe. Le lot livre la **forme** du plafond, pas sa
source externe.
- **Le prix, l'heure, le signal tarifaire** (LM-1005) — le rule-based franchit la frontière de
l'achat **grossièrement** : « plancher non atteint à 22 h → chauffer ». Une règle d'horloge.
L'optimisation appartient au MILP, donc à l'optimiseur socket, donc à 3d.
- **Le dosage en Wh du confort** — voir 8.4.
- **La conversion degrés → Wh** — couche domaine, jamais l'arbitre (LM-1008).
---
## 10. Ordre de travail proposé
| Étape | Contenu | Éprouvable |
|---|---|---|
| **10-a** | `DrawCap` / `DrawBudget`, calculés dans `buildContext()`, publiés, **sans effet sur les décisions** | oui — télémétrie, aucun changement de comportement |
| **10-b** | Délestage : le plafond borne le plan ; motif `DRAW_CAP` avec sa source ; l'ordre de coupe est l'inverse du rang | oui — simulation + banc |
| **10-c** | Refonte décider/émettre (§4) : deux passes, une commande, recrédit unique — **à comportement CONSTANT**, passe 1 vide | oui — les 16 scénarios `run` doivent rester identiques au bit près |
| **10-d** | Passe 1 réellement servie : obligation par session, régimes de LM-1009, motifs éco | oui — banc |
| **10-e** | Passe 2 confort : comparateur de sonde | oui — banc, avec la sonde du §11 |
**10-a et 10-c sont les deux étapes qui ne changent rien**, et ce sont les plus importantes :
elles rendent le reste petit. 10-c en particulier doit être prouvée par l'**invariance** des
scénarios enregistrés — si la refonte décider/émettre change un seul point de contrôle, c'est
qu'elle a changé une décision, et il faut savoir laquelle avant d'aller plus loin.

152
docs/PREPA_BANC_20260828.md Normal file
View File

@ -0,0 +1,152 @@
# Préparation du banc — arrivée V2C Trydan + Skoda Enyaq, 2026-08-28
**Écrit la veille**, depuis le code et le banc, pour que rien de ceci ne se découvre avec la
borne au mur. Le banc tourne `1.15.2+etm24`.
> **Deux blocages trouvés ce soir, et les deux sont silencieux** — ils ne produiraient aucune
> erreur, seulement des chiffres faux. Ils sont au §2. Le reste est de la logistique.
---
## 1. Ce qui n'est pas encore sur la box
**Le plugin V2C n'est PAS installé.** Le banc porte `powersync-plugin-abbb2x` et
`powersync-plugin-eastron`, rien d'autre. Le plugin `v2c` existe dans
`etm-powersync-plugins-modbus/v2c/` mais vit sur la branche `feature/v2c-trydan`, non fusionnée
(le repo est sur `fix/fronius-meter-sign-and-battery-presence`).
**À faire avant de câbler** : construire `powersync-plugin-v2c` en arm64 dans
`build-cross-arm64`, puis le poser par `deploy.sh`. Le paquet est déjà déclaré
(`debian/powersync-plugin-v2c.install`). Sans lui, la borne n'apparaîtra pas dans nymea, quelle
que soit son adresse IP.
**Statut du plugin, à connaître** : son propre brief le déclare **« Partiel — PAS dans la matrice
beta validée. Ne pas promettre tant que non testé sur borne réelle. »** Demain EST ce test.
---
## 2. LES DEUX BLOCAGES
### 2.1 `phaseCount` n'existe pas — et le moteur supposera 1 phase
La ThingClass `trydan` déclare `connected`, `pluggedIn`, `charging`, `power`,
`maxChargingCurrent`, `currentPower`, `powerL1/L2/L3` et une série d'états de diagnostic.
**Elle ne déclare pas `phaseCount`.** Et il n'existe **aucun registre de phases** dans la carte
Modbus V2C (0x0BC2–0x0BD5, 0x177A–0x1783) : le nombre de phases n'est pas lisible sur la borne.
`EvCharger::phaseCount()` retombe alors sur **1** quand l'état est absent (`evcharger.cpp:147`).
Conséquences en chaîne, toutes silencieuses, sur une Trydan **triphasée** :
| Grandeur | Calculée | Réelle | Effet |
|---|---|---|---|
| `minPowerW` (plancher publié) | 6 × 230 × **1** = 1 380 W | 6 × 230 × **3** = 4 140 W | l'arbitre croit pouvoir démarrer à 1 380 W |
| conversion W → A | `P / (230 × 1)` | `P / (230 × 3)` | consigne **3× trop haute**, écrêtée à `maxA` |
C'est **exactement la famille de défaut que 3g-2 vient de fermer** (relevé 3g-1 §3.4 : un
plancher annoncé qui n'est pas celui qui gouverne le démarrage), rouverte par le plugin au lieu
du moteur. Elle ne se verrait pas : la borne démarrerait, à un courant faux, et le waterfall
allouerait un budget qu'elle ne consomme pas.
**Correctif proposé, côté plugin V2C** : la borne ne pouvant pas le dire, le nombre de phases
doit être **saisi à l'appairage** — un `paramType` ou un `settingsType` `phaseCount`
(valeurs 1 ou 3, défaut 1), recopié dans un `stateType` `phaseCount`. C'est le geste standard
nymea pour un appareil qui ne rapporte pas sa topologie, et c'est ce que fait déjà la voiture
simulée (`phaseCount` en réglage, défaut 3).
> **Si le correctif n'est pas prêt demain matin** : câbler la Trydan **en monophasé**, ou
> n'exercer que des scénarios où le nombre de phases n'entre pas — et surtout **ne pas lire le
> `minPowerW` publié comme un plancher réel**. Le noter dans le relevé plutôt que de le corriger
> à chaud.
### 2.2 `sessionEnergy` n'est délibérément PAS exposée — et c'est LM-1009 qui en dépend
Le registre existe : `0x0BC4 ChargeEnergy`, kWh de la session. Mais le brief du plugin l'interdit
explicitement : *« NE PAS exposer en `sessionEnergy` »*, et la liste des interdits porte
« Exposer `sessionEnergy` depuis ChargeEnergy sans validation matérielle ».
La raison est bonne et vient d'ailleurs : **evcc a RETIRÉ son interface ChargeRater pour la
Trydan** (`evcc-io/evcc#28047`) — remontée d'énergie session jugée peu fiable côté firmware.
`ChargeEnergy` reste donc un état de **diagnostic**.
**Conséquence directe sur l'attendu de demain** : « `sessionEnergy` sur matériel réel » n'est
**pas franchissable** avec la V2C telle qu'elle est. Ce n'est pas un oubli, c'est un refus
documenté — et c'est demain qu'il peut être levé, parce que la validation demande précisément ce
qu'on aura : une borne réelle et un compteur de référence.
**Ce que demain permet à la place, et qui a de la valeur** :
- **Valider `ChargeEnergy`** contre une référence — intégrer `currentPower` de la borne sur une
charge complète et comparer au kWh annoncé. C'est la validation matérielle que le brief exige,
et elle débloque `sessionEnergy` pour de bon. Si l'écart est faible et stable, le câblage se
fait ensuite en une ligne de JSON.
- **Exercer la condition C1 de LM-1009 pour de vrai** : une borne sans `sessionEnergy` doit
dégrader **visiblement**. On aura le cas sur du matériel, dès le premier jour, sans le
fabriquer. Et le piège à guetter est nommé : « pas mesurable » ne doit pas s'afficher « 0 kWh
livré ».
---
## 3. La Skoda Enyaq — aucun plugin, donc voiture manuelle
**Aucune intégration Skoda / VW nulle part** dans l'espace de travail (recherche sur
`skoda|enyaq|weconnect` : zéro résultat). L'Enyaq **ne rapportera pas son état de charge**.
Elle sera donc un Thing `genericCar` — voiture *simulée*, `batteryLevel` **inscriptible** — ce
qui la place exactement sur le chemin décrit par **LM-1009** : le moteur intègre la puissance de
la borne, applique un facteur de pertes, divise par la capacité déclarée, et **réécrit
lui-même** `batteryLevel`. Le pourcentage affiché demain sera une intégration, pas une lecture.
**À saisir à la création** (rien de tout ceci n'est deviné par le moteur) :
| Réglage | Valeur | Pourquoi |
|---|---|---|
| `capacity` | capacité **utile** réelle de l'Enyaq (~58 / 77 kWh selon version) | divise l'énergie intégrée → une capacité fausse fausse tout le pourcentage |
| `minChargingCurrent` | **6** A | entre dans `max(borne, voiture)` du plancher |
| `phaseCount` | **3** (Enyaq charge en triphasé 11 kW) | plafonne le nombre de phases visé — c'est `evTargetPhases()` qui le lit |
| `batteryLevel` initial | l'état **réel** au branchement, saisi à la main | point de départ de l'intégration ; faux au départ = faux pour toute la session |
> C'est la démonstration vivante de LM-1009 : trois des quatre valeurs ci-dessus sont saisies à
> la main, et le pourcentage affiché en dépend entièrement. À noter dans le relevé.
---
## 4. Appairage de la V2C
- **Transport** : Modbus **TCP, port 502**. Il lui faut une adresse sur le LAN `192.168.1.0/24`.
- **Découverte** : la ThingClass déclare `createMethods: ["discovery", "user"]` — découverte
automatique **ou** saisie manuelle. Paramètres : `macAddress`, `address`, `hostName`, `port`.
- **IP de test documentée** : `192.168.1.127`. **Libre ce soir** — vérifié, aucune machine ne
répond à cette adresse.
- **Le piège de la Terra AC, à ne pas rejouer** : une borne qui ne répond pas à l'ARP tourne en
`ThingSetupStatusInProgress` indéfiniment. Premier geste après câblage, **avant** nymea :
`ping` depuis le banc, puis `nc -z 192.168.1.127 502`. Si l'ARP ne répond pas, le problème est
réseau, pas logiciel.
- **Registre à ne jamais écrire** : `Dynamic` (0x177E) — casse la télémétrie de la borne. La
neutralisation propre passe par `PauseDynamic` (0x1783).
---
## 5. Ce qui devient franchissable demain
| Attendu | Franchissable ? | Condition |
|---|---|---|
| **LM-1204-b** — deux bornes en ligne | **OUI** | V2C + wallbox simulée. Le critère qui justifie 3g depuis le début, enfin exerçable sans la Terra AC. |
| **Le plancher réel** — négociation de courant, délai d'établissement, véhicule qui refuse | **OUI** | et c'est ce qu'aucune wallbox simulée ne prouvera jamais |
| **`minPowerW` sans véhicule branché** | **OUI** | depuis `+etm24` la borne est *hors arbitrage* — vérifier qu'elle n'apparaît pas dans `loads[]` et que le journal le dit |
| **`EV_GRID_START`** | **PEUT-ÊTRE** | il faut que le budget tombe dans `[plancher × 0,5 ; plancher[`. À **guetter**, pas à fabriquer |
| **`sessionEnergy` sur matériel réel** | **NON** — §2.2 | remplacé par : valider `ChargeEnergy` contre une référence, et exercer C1 de LM-1009 |
---
## 6. Liste de contrôle, dans l'ordre
1. [ ] Construire `powersync-plugin-v2c` arm64 (branche `feature/v2c-trydan`) — **avant** le câblage.
2. [ ] Décider de `phaseCount` (§2.1) : ajouter le réglage d'appairage, **ou** câbler en monophasé et le noter.
3. [ ] Poser le plugin par `deploy.sh` sur `.75`.
4. [ ] Câbler la borne, lui donner `192.168.1.127`.
5. [ ] `ping` + `nc -z … 502` **depuis le banc**, avant d'ouvrir nymea.
6. [ ] Appairer la Trydan, vérifier `connected: true` et un `currentPower` qui bouge.
7. [ ] Créer la voiture `genericCar` avec les quatre réglages du §3.
8. [ ] Associer voiture ↔ V2C, mode `ChargingModeEco`, et **poser les rangs** des deux bornes
(par défaut elles arrivent toutes à 1 — cf. `RELEVE_3g2.md` §3.2).
9. [ ] Relevé : distinguer machine / test / lecture, comme `RELEVE_LOTD.md` l'exige.