From 16feb61149346fc37ce17ce1790d822f48fe4fa2 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Thu, 27 Aug 2026 15:06:37 +0200 Subject: [PATCH] =?UTF-8?q?docs(design):=20=C2=A710=20ouvert=20=E2=80=94?= =?UTF-8?q?=20=C3=A9co/confort=20avec=20le=20d=C3=A9lestage,=20et=20la=20p?= =?UTF-8?q?r=C3=A9pa=20du=20banc=20V2C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj --- AGENTS.md | 14 +- docs/DESIGN_10.md | 327 ++++++++++++++++++++++++++++++++++++ docs/PREPA_BANC_20260828.md | 152 +++++++++++++++++ 3 files changed, 491 insertions(+), 2 deletions(-) create mode 100644 docs/DESIGN_10.md create mode 100644 docs/PREPA_BANC_20260828.md 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.