From 5fb15d521bc53a68b5cc733a19f5b9d8f5ad9fef Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Thu, 27 Aug 2026 20:04:41 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec=20=C2=A714):=20oui,=20le=20stockage?= =?UTF-8?q?=20devient=20une=20charge=20rang=C3=A9e=20du=20waterfall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Question de l'agent app, relayant Patrick : « si je veux que l'énergie aille d'abord dans l'eau chaude, et seulement ensuite dans la batterie, je fais comment ? » Réponse : aujourd'hui il ne peut pas, et la réponse est OUI. Les quatre constats de l'app sont exacts, revérifiés indépendamment. Et le contrat attendait déjà cette charge : OPTIMIZER_PROTOCOL décrit adapter:"battery" dans loads[], LoadAction porte le kind Constraint avec Permission{Allow,Forbid} et Source{Solar,GridSource}, et SurplusBattery déclare reserveSocPercent / maxChargeW / maxDischargeW — trois champs qu'aucun code ne lit. domain:"battery" accepté sans effet n'est donc pas une question effleurée, c'est le reliquat visible d'une place réservée. La note dit sous quelle forme 3f la remplit. Les quatre points, tranchés : (a) Le double commandeur ne se devine pas : il se MESURE. Le plugin SunSpec écrit, relit et compare, et son commentaire nomme le cas exact — « another controller overwriting the setpoints ». Le résultat sort dans controlHealthy, déjà déclaré par la classe du banc. Trois exigences en découlent, dont celle-ci : controlHealthy == false doit SORTIR de l'arbitrage comme available == false le fait déjà, sinon c'est le trou de pluggedIn à nouveau. (b) batteryLevelConsideration et le rang ne sont pas concurrents par nature — ils le sont par accident d'implémentation. Le seuil agit aujourd'hui comme un rang absolu ; quand la batterie sera rangée, il perd ce rôle et ne garde que celui de plancher. Le mouvement a commencé en 3g-1 avec le défaut passé de 0,9 à 0,2, précisément parce que 0,9 confondait cible et réserve. (c) La sûreté : « ne pas charger » et « décharger » sont deux gestes que le mot unique cachait. Le premier ne vide rien et c'est la demande de Patrick ; le second est déjà refusé par le budget, quel que soit le niveau. Un rang ne fait jamais descendre un SOC, il décide de ce qui le fait monter. (d) Le signe : l'inquiétude tombe. Charger une batterie, c'est consommer — au compteur c'est indistinguable d'un chauffe-eau, donc allocatedW se réconcilie sans traitement particulier. Le sens opposé n'apparaît qu'à la décharge, et une décharge n'est pas une allocation : c'est une source, elle appartient au revenu du budget. Règle : allocatedW d'une batterie est toujours ≥ 0. La règle Rules.* écartée par l'app l'est pour la bonne raison, et la note le confirme : ce serait un second commandeur sans arbitre, pire que celui de 3g-2 puisqu'il vivrait hors du moteur, donc hors du journal. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj --- specs/spec_loadmodel.md | 173 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 954c1c3..1c6dc16 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -1094,3 +1094,176 @@ derrière une seule. sonde et du diagnostic de mesure (§11), puisqu'elle n'a aucun effet sur les décisions. --- + +--- + +## §14 — Le stockage est-il une charge arbitrable ? + +> **Statut : réponse de conception. Ne déclenche aucun travail** — elle dit sous quelle forme +> l'étape **3f** (déjà déférée : *« BatteryAdapter — constraints + charge réseau plafonnée + +> waterfall grid-funding »*) devra se faire, et lève les quatre points que l'agent app a nommés. + +**La demande, telle que Patrick l'a formulée** : *« si je veux que l'énergie aille d'abord dans +l'eau chaude, et seulement si l'eau est chaude je commence à charger la batterie, je fais +comment ? »* + +**Réponse courte : aujourd'hui il ne peut pas, et la réponse est OUI — le stockage doit devenir +une charge du waterfall.** Ce n'est pas une porte à ouvrir : elle est déjà encadrée, et depuis le +début. + +--- + +### LM-1400 — Les quatre constats sont exacts, vérifiés indépendamment + +| Constat | Vérifié | +|---|---| +| `batteryLevelConsideration` donne la priorité à la BATTERIE, pas aux charges | oui — `rulebasedscheduler.cpp:125-139`, sous le seuil le budget est **annulé** | +| La batterie n'est dans aucun waterfall | oui — aucune entrée `evcharger`/`battery` dans `GetLoadConfig` ; quatre charges sur `.75` | +| Le moteur ne la commande nulle part | oui — `enableCharging`, `enableDischarging`, `chargingRate`, `dischargingRate`, `gridCharging` : **0 occurrence** dans `energyplugin/` | +| Le levier existe | oui — la classe `sunspecStorage` du banc déclare **les cinq actions**, et l'énumération `domain` accepte déjà `battery` sans que rien n'en fasse quoi que ce soit | + +**Et le contrat l'attendait.** `OPTIMIZER_PROTOCOL.md` décrit déjà la batterie comme une charge de +`loads[]` (`"adapter":"battery"`), avec **deux kinds** dont un lui est réservé : + +| kind | ce qu'il exprime | déjà déclaré | +|---|---|---| +| `setpoint` + `source: "grid"` | charger la batterie, éventuellement **depuis le réseau** | `LoadAction::Source { Solar, GridSource }` | +| `constraint` | `charge`/`discharge` : `allow` \| `forbid` | `LoadAction::Permission { Allow, Forbid }` | + +`SurplusBattery` porte déjà `reserveSocPercent`, `maxChargeW`, `maxDischargeW` — trois champs +qu'aucun code ne lit aujourd'hui. **Le modèle a été conçu pour ça et n'a jamais été câblé.** +`domain: "battery"` accepté sans effet n'est donc pas une question effleurée : c'est le reliquat +visible d'une place réservée. + +--- + +### LM-1401 — (a) Le double commandeur : la réponse est MESURABLE, pas à deviner + +C'est la bonne question, et c'est la leçon de 3g-2 correctement appliquée : **avant d'ajouter un +commandeur, savoir qui commande déjà**. Mais ici, contrairement aux bornes, **le détecteur existe +déjà et il est publié**. + +Le plugin SunSpec **écrit, relit, et compare** (`integrationpluginsunspec.cpp:703-726`), et son +commentaire nomme exactement le cas redouté : + +> *« An inverter may accept a write and then ignore it without raising a Modbus exception — +> control not enabled on the device, grid charging not allowed, an IP restriction, or **another +> controller overwriting the setpoints**. An unverified setpoint is an unapplied setpoint. »* + +Le résultat sort dans l'état **`controlHealthy`**, déjà déclaré par la classe du banc. + +**Conséquence pour 3f, et elle est forte** : la question « l'onduleur obéit-il ? » ne se tranche +pas sur le papier — **elle s'observe**. Trois exigences en découlent : + +1. **Mesurer avant de commander.** Une consigne d'essai, `controlHealthy` relu. Si l'onduleur + re-décide, il le dira. C'est exactement ce qui manquait aux bornes avant 3g-2, et qui a coûté + un lot entier. +2. **`controlHealthy == false` doit sortir de l'arbitrage**, comme `available == false` le fait + déjà pour une charge en défaut (ECS-410) : une batterie dont les consignes ne prennent pas + n'est pas pilotable, et lui allouer du budget serait le trou de `pluggedIn` à nouveau. +3. **Le motif doit le dire.** Une batterie non servie parce que l'onduleur reprend la main est un + diagnostic d'installation, pas un manque de surplus. Deux causes, deux gestes. + +--- + +### LM-1402 — (b) `batteryLevelConsideration` cesse d'être un rang et redevient une RÉSERVE + +Les deux mécanismes ne sont pas concurrents **par nature** — ils le sont par accident +d'implémentation. Ce sont deux objets différents que le code actuel confond : + +| | ce que c'est | ce que le code en fait aujourd'hui | +|---|---|---| +| **rang** (`priority`) | l'ordre de service : qui reçoit le surplus en premier | n'existe pas pour la batterie | +| **réserve** (`batteryLevelConsideration`) | un plancher : sous ce niveau, on protège le stockage | **agit comme un rang absolu** — annule tout le budget des autres | + +**Position : quand la batterie devient une charge rangée, le seuil PERD son rôle de priorité et +ne garde que son rôle de plancher.** « La batterie d'abord » s'exprime alors par le rang — et +« l'eau chaude d'abord » aussi, dans la même liste, par le même geste. + +Le précédent existe et il va dans ce sens : le défaut d'usine est passé de `0,9` à `0,2` en +3g-1, précisément parce que **`0,9` confondait une cible et une réserve** (LM-1203-b). Ce lot +finit ce mouvement. + +> **Ne pas garder les deux rôles.** Deux réglages dont l'un défait l'autre en silence est +> exactement ce que LM-1207 refuse pour les priorités : deux classements qui ne s'interclassent +> pas ne sont pas deux fonctionnalités, c'est un défaut. Et l'écran ne saurait pas le présenter. + +--- + +### LM-1403 — (c) La sûreté : deux gestes très différents sous un seul mot + +« Le rang peut-il vider une batterie ? » — la question se scinde, et la scission désamorce la +moitié de l'inquiétude : + +| Geste | Effet sur le SOC | Dangereux ? | +|---|---|---| +| **Ne pas la charger** (servir l'ECS d'abord) | le SOC **ne monte pas** | **non** — réversible, le soleil revient. C'est la demande de Patrick, et elle ne vide rien | +| **La décharger pour servir une charge** | le SOC **descend** | **oui** — et c'est le seul cas à border | + +Or le second est **déjà refusé aujourd'hui**, sans rapport avec le rang +(`rulebasedscheduler.cpp:152-156`) : + +```cpp +if (ctx.battery.powerW < 0) { + // Décharge : ne pas vider la batterie pour alimenter une charge pilotée, quel que + // soit le niveau. Ce qu'elle fournit n'est pas du surplus. + remainingSurplusW += ctx.battery.powerW; // powerW < 0 → retranche +} +``` + +**Position** : le rang gouverne uniquement l'**ordre de charge**. Trois planchers restent +intouchables et ne se règlent pas par glissement — `batteryCritical`, `reserveSocPercent` (déjà +au contrat, jamais lu), et l'interdiction ci-dessus. **Un rang ne fait jamais descendre un SOC ; +il décide seulement de ce qui le fait monter.** Le glissement demandé est donc sûr par +construction, et c'est ce qui permet de l'offrir sans écran d'avertissement. + +--- + +### LM-1404 — (d) Le signe : l'inquiétude tombe, et pour une raison précise + +La crainte est légitime après `EV_GRID_START`, mais elle ne s'applique pas ici. **Charger une +batterie, c'est consommer** — au compteur, c'est indistinguable d'un chauffe-eau qui chauffe. + +| | `allocatedW` veut dire | signe au bilan | +|---|---|---| +| ECS | consommer 2 000 W | **−** au surplus | +| Borne | consommer 2 000 W | **−** au surplus | +| **Batterie en charge** | **stocker** 2 000 W | **−** au surplus — **identique** | + +`allocatedW` d'une batterie qui charge se réconcilie donc avec `budget.allocatedW` **sans aucun +traitement particulier** : c'est une charge comme les autres, et le waterfall n'a pas à savoir ce +qu'elle fait de l'énergie. + +**Le sens opposé n'apparaît qu'à la DÉCHARGE — et une décharge n'est pas une allocation.** C'est +une *source*, elle appartient au revenu du budget, pas à sa dépense. Le code le traite déjà ainsi +(LM-1403). **Les mélanger serait le vrai défaut** : une décharge comptée en `allocatedW` négatif +rendrait faux l'invariant publié `surplus + recrédit − alloué == restant`. + +> **Règle qui en sort** : `allocatedW` d'une batterie est **toujours ≥ 0**. Ce qu'elle rend au +> réseau ou à la maison n'est jamais une allocation. + +--- + +### LM-1405 — Ce qui reste à décider, et que cette note ne tranche pas + +- **La charge réseau plafonnée** (`setpoint` + `source: "grid"`) : acheter pour stocker est un + franchissement de frontière, le même que le §10. Il doit suivre les mêmes règles — plafond de + soutirage de LM-1006-1 compris. **Hors de ce qui répond à la demande de Patrick** : servir + l'ECS avant la batterie ne demande aucun achat. +- **L'ordre relatif par défaut.** Une installation existante a aujourd'hui « batterie d'abord » + par le seuil. Lui donner un rang change son comportement : quel rang par défaut, et + faut-il migrer les installations en service ? Même famille que le rang par défaut des bornes, + et même risque — un ordre qui n'est pas un choix mais un artefact (`RELEVE_3g2.md` §3.2). +- **La forme de l'entrée `LoadConfig`.** `adapter: "battery"`, sur le modèle de `evcharger` + (rang seul, limites lues sur le Thing) — à confirmer à l'écriture de 3f. + +### LM-1406 — Ce qu'il ne faut PAS faire en attendant + +**Une règle `Rules.*` coupant `enableCharging` fonctionnerait, et elle est à écarter.** L'agent +app l'a écartée de lui-même, pour la bonne raison : ce serait un **second commandeur sans +arbitre** — précisément le défaut que 3g-2 vient de supprimer sur les bornes, réintroduit par la +porte de l'app. Et il serait pire ici : sur les bornes, le second commandeur était au moins +*dans* le moteur, donc observable au journal. + +**« Pas réglable aujourd'hui » est la bonne réponse à donner à l'utilisateur**, et elle vaut +mieux qu'un réglage dont personne ne garantit l'effet.