11.1 étalement uniforme, avec les trois choses qui s'écrivent avec lui : recalculé à chaque cycle (une cadence figée ne rattrape pas et manque l'échéance en ayant l'air de travailler) ; il se durcit à l'approche — 2 kWh font 2 kW à une heure et 12 kW à dix minutes, donc la fenêtre d'achat massif arrive d'un coup et c'est le MOTIF qui doit le dire ; et un point de non-retour explicite, parce qu'un seuil nommé se discute là où une asymptote se subit. 11.2 oui aux quatre champs, et la subtilité entre dans le CONTRAT : sous la réserve batterie, draw.remainingW reste non nul pendant que budget.remainingW est à zéro. LM-1203-b vue depuis la frontière — la réserve annule le surplus, jamais l'autorisation de soutirage. 11.3 dissoute, 11.4 par champ explicite. Et un troisième mode d'érosion dans AGENTS.md, symétrique des deux autres : non pas une règle qui se périme faute d'être exécutable, mais une règle rendue IMMORTELLE par sa propre garde. draw.authorisedW a été refusé pour un motif juste, épinglé par un test, puis le motif est tombé — et la formulation a survécu, protégée par un test qui ne savait pas que le monde avait changé. Parade déjà pratiquée ici : quand un test fige une ABSENCE, son commentaire écrit la condition de levée. « Absent tant que rien ne le mesure » se relit et se lève ; « ne doit pas exister » se relit et s'obéit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
543 lines
32 KiB
Markdown
543 lines
32 KiB
Markdown
# 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
|
||
> points qui demandent une décision** — §8. **Quatre sont tranchés.** 2026-08-27 : la réserve
|
||
> batterie n'annule pas l'autorisation de soutirage (8.1), et le repli dégradé ne concerne pas ce
|
||
> lot (8.3). 2026-08-29 : le plafond §14a porte **les deux** formes — par phase et au total —
|
||
> sans jamais convertir (8.2), et la cible de confort reste un **comparateur**, le dosage
|
||
> attendant le modèle thermique du §11 (8.4).
|
||
>
|
||
> **Un seul reste ouvert, et il est chez l'app** : 8.5, l'ordre de service apparent. Question
|
||
> posée le 2026-08-29 dans `docs/BRIEF_depuis_plugin.md` §9.
|
||
|
||
---
|
||
|
||
## 0. ⚠️ CE QUI A CHANGÉ DEPUIS L'ÉCRITURE DE CE DESIGN (2026-08-30)
|
||
|
||
Ce document a été écrit **avant** le lot délestage. Trois de ses sections décrivent désormais du
|
||
code livré, et une propose une forme qui a depuis été **refusée**. Lire les sections suivantes
|
||
sans cette mise à jour ferait reconstruire l'existant, ou pire, ressusciter un champ qu'un test
|
||
interdit.
|
||
|
||
| Section | État |
|
||
|---|---|
|
||
| **§2 — le plafond de soutirage devient un OBJET** | **LIVRÉ.** `DrawCap` / `DrawMargin` / `resolveDrawMargin()`, avec les trois sources et le départage par rang de contrainte. Voir `DESIGN_DELESTAGE.md`. |
|
||
| **§5 — le délestage, ce qu'il coupe et dans quel ordre** | **LIVRÉ** (`LM-1211`) : ordre inverse du rang, franchissement de verrou **constaté** et non présumé, réservé aux plafonds subis. |
|
||
| **§6 — les motifs** | `DRAW_CAP` **existe**, avec sa source, son ampleur et sa phase. `ECO_FLOOR_MET` / `ECO_FLOOR_GRID` restent à naître. |
|
||
| **§3 — la forme de `PlanDraw`** | **PARTIELLEMENT REFUSÉE**, voir ci-dessous. |
|
||
|
||
### La correction à faire sur §3 : `PlanDraw` n'a qu'un champ
|
||
|
||
Le §3 propose `PlanDraw { authorisedW, committedW, remainingW }`. Seul **`committedW`** a été
|
||
livré, et l'absence des deux autres a été **défendue en prose à trois endroits** puis **épinglée
|
||
par un test** (`testLoadTelemetryRpc` casse si on ajoute le champ). Le motif d'alors : *« un
|
||
`authorisedW` forgé afficherait un plafond de soutirage qui n'existe pas »*.
|
||
|
||
> **Mais ce motif est tombé.** Le plafond existe désormais — `ctx.drawMargin`, résolu une fois
|
||
> par cycle, avec sa source et sa phase. `authorisedW` ne serait plus une forge : il serait une
|
||
> lecture. **C'est donc une décision à reprendre, pas une règle à appliquer** (voir §11.2), et
|
||
> c'est un bon exemple de ce qu'`AGENTS.md` appelle une décision qui se périme : elle était
|
||
> juste, son motif a disparu, et seule sa formulation a survécu.
|
||
|
||
---
|
||
|
||
## 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. **Réservé à l'autorisation résiduelle NULLE** — voir la précédence ci-dessous. |
|
||
|
||
**PRÉCÉDENCE `DRAW_CAP` ↔ `BELOW_MIN_POWER`** — tranchée le 2026-08-29, et son domicile est
|
||
`specs/spec_loadmodel.md` **LM-1011**, pas ici : une note de conception se périme, la spec fait
|
||
autorité. En un mot : autorisation résiduelle **non nulle mais sous le plus petit palier** →
|
||
`BELOW_MIN_POWER` ; `DRAW_CAP` réservé au résiduel **nul**. Le point de rupture de l'écran 2 est
|
||
ancré sur le `DRAW_CAP`.
|
||
|
||
**RÉPONSE À R4 — ce que les compteurs de budget comptent : le DÉCIDÉ.** *(Question de l'agent
|
||
app, 2026-08-29.)*
|
||
|
||
`budget.allocatedW` et ses voisins comptent **ce que la cascade a décidé**, jamais ce que le
|
||
matériel a reçu. Ce n'est pas un choix de commodité : le budget est le **registre de la
|
||
cascade**, et ce qui détermine ce que reçoit la charge suivante est la valeur décidée. L'arrondi,
|
||
lui, vit **après** — dans l'adaptateur, au dispatch, et il est propre à chaque mécanisme. Compter
|
||
le commandé obligerait à **rouvrir le budget après le dispatch**, c'est-à-dire à décider deux
|
||
fois.
|
||
|
||
> ⚠️ **Et cela corrige une prémisse de R4, qu'il vaut mieux corriger maintenant.** La maquette
|
||
> écrit « `allocatedW` publie ce qui a été **commandé** ». **C'est faux aujourd'hui** :
|
||
> `buildTelemetry()` publie `action.estimatedPowerW` du plan, et `applyActionsToAdapters()`
|
||
> reçoit le slot **`const`** — rien n'est réécrit après le dispatch. Mesurable :
|
||
> `testUnpluggedChargerTakesNoBudget` attend `allocatedW == 3000` sur une borne dont le courant
|
||
> commandé donne 2 990 W.
|
||
>
|
||
> Donc **`Σ targetW == allocatedW` tiendra exactement**, par construction — les deux sont du
|
||
> décidé. L'écart que l'écran veut nommer (« 1 400 W décidés → palier 1 500 W ») n'est **pas**
|
||
> entre `targetW` et `allocatedW` : il est entre `allocatedW` (**décidé**) et la charge utile de
|
||
> mécanisme (**appliqué**) — `mechanism.stageW`, `mechanism.setpointW`, `mechanism.currentA` —
|
||
> **qui est déjà publiée aujourd'hui**. L'écran a déjà les deux nombres ; ce sont leurs noms qui
|
||
> étaient inversés.
|
||
| `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 POINTS À TRANCHER — deux le sont déjà
|
||
|
||
**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).
|
||
|
||
**TRANCHÉ (Patrick, 2026-08-27) : 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. Et la réserve garde son sens là où elle en a un —
|
||
empêcher de vider la batterie dans un chauffe-eau — ce qu'un soutirage réseau ne fait pas.
|
||
Inscrit en LM-1203-b.
|
||
|
||
**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 ? TRANCHÉ, et la réponse retire le point du lot.**
|
||
*(Patrick, 2026-08-27.)* Les deux cas ne se confondent pas : le plafond du §10 est
|
||
**auto-imposé**, donc un repli qui **ne dépense pas** le respecte par construction — rien à
|
||
écrire. L'exigence dure — tenir une borne imposée par un TIERS alors que le moteur ne décide
|
||
plus — appartient au **§14a**, dont le transport n'existe pas, et elle vivra **hors du moteur**.
|
||
Les confondre ferait hériter au §10 d'une exigence qui ne le concerne pas encore. LM-1006-1 est
|
||
mis à jour en conséquence : **le §10 n'a rien à faire de ce côté.**
|
||
|
||
**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 ? POSÉE À L'AGENT APP le 2026-08-29**, et c'est le
|
||
dernier point ouvert du lot. LM-1006, couplage 2 : un plancher éco de rang 4 passe devant un
|
||
confort de rang 1 — voulu, et **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**.
|
||
|
||
La question est formulée en `docs/BRIEF_depuis_plugin.md` §9, avec les trois choses à trancher —
|
||
sur quoi la liste est ordonnée, une entrée ou deux par charge, et ce qu'on voit d'une charge dont
|
||
l'éco est servi et le confort non. **Le moteur ne répond pas à sa place** : ce qui lui est demandé
|
||
est de dire quelle forme de publication rendrait l'écran possible, comme la maquette des
|
||
mécanismes l'a fait pour les six écrans. La charge utile suivra.
|
||
|
||
> ⚠️ **Tant qu'elle n'est pas tranchée, le §10 ne se code pas.** Ce n'est pas une dépendance
|
||
> molle : la troisième question — une charge dont l'éco est servi et le confort non — n'a
|
||
> **aucun équivalent** dans la télémétrie d'aujourd'hui, qui publie un motif et une allocation,
|
||
> un seul de chaque. Commencer par le moteur reviendrait à inventer une charge utile puis à
|
||
> demander à l'écran de s'en arranger, c'est-à-dire l'ordre inverse de celui qui a marché.
|
||
|
||
---
|
||
|
||
## 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 — trois étapes à comportement CONSTANT, puis une seule qui change
|
||
|
||
*(Réécrit le 2026-08-30, après le lot délestage. La question de forme du §9 est tranchée : la
|
||
maquette `deux_passes_eco_confort_mockup.html` **fait contrat**, et `levels[]`, `funding` et
|
||
`counts` sont déjà livrés.)*
|
||
|
||
La stratégie du délestage a marché et se réemploie telle quelle : **ce qui coûte dans un lot de
|
||
ce genre n'est pas d'écrire le comportement, c'est de prouver qu'on n'a rien cassé en chemin.**
|
||
Trois étapes ne changent rien et rendent la quatrième petite.
|
||
|
||
### Étape A — le SPLIT décider / émettre, à comportement constant
|
||
|
||
C'est le gros du travail, et il n'ajoute aucune fonction. `getPlan()` se scinde en deux temps :
|
||
une phase qui calcule une **cible par charge**, une phase qui **émet une commande et une seule**.
|
||
Aujourd'hui `buildSetpointAction()` décide, écrête, comptabilise et motive dans le même geste, et
|
||
décrémente le budget en même temps qu'il émet — ce qui rend une seconde passe littéralement
|
||
inexprimable.
|
||
|
||
Tant qu'il n'y a qu'une passe, la cible confort est la seule, et la sortie doit être **identique
|
||
au watt près**. C'est ce qui rend l'étape vérifiable : `simulation` et `charging` ne doivent pas
|
||
bouger d'une ligne. Le filet est déjà là — les tests de motif portent sur le **texte publié**,
|
||
donc ils survivront au remaniement ou le condamneront.
|
||
|
||
> **Le piège à surveiller.** L'arrondi au mécanisme et l'écrêtage de verrou doivent migrer vers
|
||
> la phase d'émission **sans changer d'ordre relatif**. `ECS-306` (clamp lock-aware AVANT de
|
||
> décrémenter le budget) et `ECS-309` (le motif nomme le mécanisme réel, via `vouluW`) reposent
|
||
> tous deux sur cet ordre. Les déplacer d'un cran ferait publier des motifs faux sans qu'aucun
|
||
> nombre ne change — le genre de régression qu'une somme correcte masque.
|
||
|
||
### Étape B — `draw` complété, publication SEULE
|
||
|
||
`draw` ne publie que `committedW`. Le plafond existe maintenant : `binding` (la source qui
|
||
borne), `perPhaseBound`, `authorisedW` et `remainingW` deviennent des **lectures** et non des
|
||
forges. Aucune décision ne change ; c'est un ajout de charge utile, et il ferme la moitié de R8
|
||
qui restait ouverte au niveau du cycle.
|
||
|
||
Voir la décision **§11.2** — l'ajout suppose de revenir sur un refus dont le motif est tombé, et
|
||
de mettre à jour le test qui l'épingle.
|
||
|
||
### Étape C — l'AVANCEMENT mesuré, publié sans rien décider
|
||
|
||
`LoadContextTelemetry::sessionWh` est **déclaré et rempli nulle part** ; `EvCharger` n'expose
|
||
même pas d'accesseur vers `sessionEnergy`, que quatre des cinq classes du banc publient.
|
||
`minEnergyWhPerDay` est transporté par la config et le RPC, et **lu par aucune décision**. Les
|
||
deux moitiés de l'obligation éco sont donc de la plomberie inerte.
|
||
|
||
Cette étape les branche **en lecture seule** : `sessionWh` alimenté, `progress` publié dans
|
||
`levels[]` avec son `regime` ∈ `measured | estimated | unmeasurable`, `deliveredWh` **absent**
|
||
quand le régime est `unmeasurable` — jamais à zéro, sans quoi une dégradation se lirait comme un
|
||
avancement nul, ce que `LM-1009 C1` interdit nommément.
|
||
|
||
> **⚠️ `sessionEnergy` porte un OFFSET — mesuré le 2026-08-31.** Sur la V2C, l'écart entre
|
||
> `sessionEnergy` et `chargeEnergy` est constant au dix-millième (7,2044), parce que
|
||
> l'accumulateur du plugin porte les incréments d'avant la dernière remise à zéro. **La valeur
|
||
> absolue n'est donc pas l'énergie de la session courante.** `deliveredWh` doit être une
|
||
> **différence depuis une base capturée**, jamais une lecture directe — sans quoi toute
|
||
> obligation éco se croirait tenue de 7,2 kWh dès le premier cycle. Un nombre crédible et faux,
|
||
> que rien ne signalerait. Détail et mesures : `RELEVE_APP_20260831.md`.
|
||
>
|
||
> Corollaire : **la V2C n'est plus un cas `unmeasurable`.** `LM-1009 C1` reste vraie pour les
|
||
> bornes sans l'état, mais le porteur d'exemple change — R6 devra citer une autre classe.
|
||
|
||
> **C'est ici que R5 devient vraie, et R6 devient TESTABLE avant d'être nécessaire.** Le régime
|
||
> se publie sans qu'aucune décision ne s'y appuie : l'app peut afficher « non mesurable » sur la
|
||
> V2C dès cette étape, et la garde de R6 — `ECO_FLOOR_MET` interdit sous `unmeasurable` — aura
|
||
> son régime à interroger le jour où le motif naît. On sépare **publier le régime** de **décider
|
||
> avec**, exactement comme `measurement` l'a été de l'arbitrage.
|
||
|
||
### Étape D — la PASSE ÉCO naît, et c'est la seule étape qui change le comportement
|
||
|
||
Deux passes sur la même liste triée, `ECO_FLOOR_MET` et `ECO_FLOOR_GRID` au catalogue, R6 armée.
|
||
La passe 1 est bornée par l'**autorisation de soutirage**, la passe 2 par le **reliquat de
|
||
surplus**. Le délestage la rattrape si tous les planchers se servent au réseau le même matin —
|
||
c'est la précondition qui vient d'être levée.
|
||
|
||
---
|
||
|
||
## 11. CE QUI DEMANDE UNE DÉCISION AVANT L'ÉTAPE D
|
||
|
||
**11.1 — Comment une obligation en ÉNERGIE devient-elle une cible en WATTS pour ce cycle ?**
|
||
C'est la question centrale, et le design ne l'a jamais tranchée. `needs` porte
|
||
`minEnergyWhPerDay`, une `deadline` et un `targetSocPercent` — jamais un plancher en watts. La
|
||
passe éco doit donc convertir « il reste 900 Wh à livrer avant 7 h » en « prends 1400 W
|
||
maintenant ».
|
||
|
||
Trois formes possibles, et elles ne se valent pas :
|
||
|
||
| Forme | Ce qu'elle donne | Ce qu'elle coûte |
|
||
|---|---|---|
|
||
| **Tout ou rien** | la charge prend son plancher dès que l'obligation n'est pas tenue | achète au pire moment, et fait disjoncter à plusieurs |
|
||
| **Étalement uniforme** | `reste / temps restant` | simple, vérifiable, mais ignore le tarif et le soleil à venir |
|
||
| **Au plus tard** | ne rien acheter tant que le surplus peut encore suffire | optimal, mais suppose une **prévision** — hors modèle |
|
||
|
||
**TRANCHÉ (2026-08-31) : l'étalement uniforme**, pour la même raison que `LM-1008` a fait de la
|
||
cible de confort un **comparateur** plutôt qu'un dosage : ce qui manque n'est pas la formule,
|
||
c'est le modèle qui la rendrait honnête. Un étalement se lit, se teste, et ne prétend rien
|
||
prévoir. Le « au plus tard » viendra avec la prévision, pas avant.
|
||
|
||
**Trois choses s'écrivent AVEC lui, et aucune n'est un détail :**
|
||
|
||
**(a) Recalculé à CHAQUE cycle, jamais figé au départ.** Une cadence calculée une fois au
|
||
branchement ne rattrape pas : après une heure sans surplus, elle reste à la cadence initiale et
|
||
**manque l'échéance** en ayant l'air de travailler. L'étalement est un rapport entre ce qui
|
||
reste à livrer et le temps qui reste — les deux bougent, donc il se relit.
|
||
|
||
**(b) Il se DURCIT à l'approche, mécaniquement, et cela doit être DIT.** 2 kWh restants font 2 kW
|
||
à une heure de l'échéance et **12 kW à dix minutes**. La fenêtre où l'achat devient massif
|
||
n'arrive pas progressivement : elle arrive **d'un coup**. Sans que le motif le nomme, un client
|
||
verra sa facture grimper à 6 h du matin sans comprendre — et il aura raison de chercher une
|
||
panne. C'est donc le **motif publié** qui porte la cadence et le temps restant, pas seulement les
|
||
watts : la même doctrine que partout ici — **publier le régime, ne jamais le laisser déduire.**
|
||
|
||
**(c) Un POINT DE NON-RETOUR explicite**, comme la maquette batterie le demandait : passé une
|
||
heure limite, on achète ce qu'il faut. Sans lui, l'étalement peut théoriquement tendre vers
|
||
l'infini sans jamais décider — et un seuil nommé se discute, là où une asymptote se subit.
|
||
|
||
**11.2 — Publie-t-on `draw.authorisedW` et `remainingW` maintenant ?**
|
||
Le refus tenait à ce qu'ils n'existaient pas. Ils existent. Mais `remainingW` a une subtilité :
|
||
sous la réserve batterie, `LM-1203-b` annule le budget de surplus **sans annuler l'autorisation
|
||
de soutirage** — donc `draw.remainingW` reste non nul quand `budget.remainingW` est à zéro, et
|
||
c'est **juste**. Il faut le dire, sinon un client conclura à une incohérence.
|
||
**TRANCHÉ (2026-08-31) : oui aux quatre**, et le test qui les interdit se met à jour en même
|
||
temps — changement de contrat assumé, pas régression. Son nouveau commentaire dira **à quelle
|
||
condition il devient faux**, ce que l'ancien ne disait pas (cf. `AGENTS.md`, troisième mode
|
||
d'érosion).
|
||
|
||
**Et la subtilité s'écrit EN TOUTES LETTRES DANS LE CONTRAT, pas en note de bas de page :**
|
||
sous la réserve batterie, **`draw.remainingW` reste non nul pendant que `budget.remainingW` est à
|
||
zéro.** C'est `LM-1203-b` vue depuis la frontière — la réserve annule le **surplus**, jamais
|
||
l'**autorisation de soutirage**. Un client y verra une incohérence, et il aura raison de la
|
||
signaler si personne ne l'a dite.
|
||
|
||
**11.3 — La passe éco achète-t-elle au réseau par DÉFAUT ?**
|
||
`ECO_FLOOR_GRID` dit « ce plancher est servi en achetant ». Mais faut-il acheter dès que le
|
||
surplus manque, ou seulement quand l'échéance est menacée ? Avec l'étalement uniforme la question
|
||
se dissout : on achète ce que l'étalement réclame **et rien de plus**, donc jamais « par défaut »
|
||
— toujours parce que le temps restant l'exige. **TRANCHÉ : dissoute — pas de décision séparée**, elle
|
||
est absorbée par 11.1.
|
||
|
||
**11.4 — Que devient le recrédit anti-clignotement avec deux passes ?**
|
||
`LM-1004` fixe qu'il est attribué **une seule fois**. Reste à dire **où** : à la première passe
|
||
où la charge apparaît. Une charge à plancher éco le reçoit donc en passe 1, une charge sans
|
||
obligation en passe 2. **TRANCHÉ : un champ explicite dans la cible** (`recreditAttribue`)
|
||
plutôt qu'une discipline — la discipline ne se teste pas, le champ si.
|
||
|
||
---
|
||
|