diff --git a/AGENTS.md b/AGENTS.md index 041e48b..965d64e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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). diff --git a/docs/DESIGN_10.md b/docs/DESIGN_10.md new file mode 100644 index 0000000..7bc868b --- /dev/null +++ b/docs/DESIGN_10.md @@ -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 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 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 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. diff --git a/docs/PREPA_BANC_20260828.md b/docs/PREPA_BANC_20260828.md new file mode 100644 index 0000000..88de59e --- /dev/null +++ b/docs/PREPA_BANC_20260828.md @@ -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.