# Brief agent app — ce que 3g-2 change pour l'écran **Émis par** : l'agent plugin, après 3g-2 · **Date** : 2026-08-27 **Banc** : `.75`, plugin `1.15.2+etm24`, nymea `1.15.2+202606191336~trixie1` **Contexte** : `docs/RELEVE_3g2.md` (ce dépôt) porte les traces machine, cycle par cycle. **Contrat** : `INTERFACE.md` fait autorité — ce brief ne fait qu'en signaler les mouvements. > Huit points. Le sixième répond à vos quatre constats du 28 août, le septième à votre > constat d'usage sur le compteur par charge — et celui-là **bouge un champ du contrat**. > Le deuxième **retire une mise en garde** que vous portiez — du code conditionnel > peut disparaître. Le cinquième répond à **votre** question sur `targetPercentage`, et c'est > celui qui change un écran : l'avancement s'affichera en énergie livrée, pas en pourcentage. --- ## 1. `loads[] ⊆ GetLoadConfig` est RÉTABLI 3g-1 avait rompu l'invariant sans le signaler : les bornes entraient dans `loads[]` de `GetLoadTelemetry` **sans figurer dans aucune `GetLoadConfig`**. C'était exactement la charge fantôme que le lot B-bis avait supprimée, réapparue par l'autre bout — et vous vous appuyez sur cet invariant pour résoudre libellé, domaine et rang de chaque charge publiée. **Depuis `+etm23`, toute borne détectée reçoit d'office une entrée `LoadConfig`.** Sur le banc, `GetLoadConfig` est passé de **2 à 4 entrées** au premier cycle après la mise à jour : ``` chauffe-eau adapter=relay-router prio=1 domain=ecs PAC banc adapter=sg-ready prio=2 domain=heating Simulated wallbox adapter=evcharger prio=1 domain=ev ← nouvelle Terra AC Charger (TCP) adapter=evcharger prio=1 domain=ev ← nouvelle ``` Forme d'une entrée `evcharger` : `id` (le ThingId de la borne), `adapter: "evcharger"`, `mode: "dynamic"`, `domain: "ev"`, plus `label` / `priority` / `enabled`. **Aucune charge utile** — pas de `relays`, pas de `sgReady`, pas de `powerLevels`/`maxPowerW`/`minPowerW`, pas de `minOnS`/`minOffS`. Les limites d'une borne viennent du Thing et changent avec le véhicule branché ; les déclarer en configuration en ferait une seconde source de vérité. **Rien à faire de votre côté pour que ça marche** : l'aller-retour `GetLoadConfig` → `SetLoadConfig` verbatim est **neutre**, vérifié sur machine — l'objet relu est identique et aucun adaptateur n'est reconstruit. **Le sens de l'inclusion reste le même** : `GetLoadConfig` peut contenir des charges absentes de `loads[]`. Pour une borne, ça veut dire *hors arbitrage* — pas de voiture assignée, mode manuel, ou **aucun véhicule branché** (ce dernier cas depuis `+etm24`). Une borne configurée absente de `loads[]` se lit « pas arbitrée en ce moment », jamais « perdue ». --- ## 2. ⚠️ La mise en garde sur `allocatedW` des bornes TOMBE Vous portiez : *« pour une borne, `allocatedW` est la décision de l'arbitre et non l'état de la borne »*. C'était juste, et ça ne l'est plus. **Le défaut derrière cette mise en garde** : jusqu'à `+etm22`, l'arbitre décidait et publiait, puis `adjustEvChargers()` — le chemin d'exécution hérité de l'amont — re-décidait derrière lui et commandait autre chose. La décision publiée n'atteignait pas le matériel. **`+etm23` ferme ça.** Une charge, un commandeur : les actions EV du waterfall sont dispatchées vers l'adaptateur, et les bornes ainsi commandées sortent du chemin proxy. Mesuré sur machine : 9 commandes émises à la borne sur la fenêtre d'observation, **9 par l'arbitre, 0 par le proxy**, et 6 cycles sur 6 portent la ligne « commandée par le waterfall, pas de re-décision ici ». **Conséquence pour vous** : `allocationIsCommand` doit rendre `true` **partout**, et les branches conditionnelles qui en dépendaient peuvent disparaître. **Une réserve, et elle est petite mais réelle** : l'allocation reste ce que l'arbitre a **commandé**, pas ce que la borne **tire**. Un plafond matériel, un véhicule qui refuse ou un câble débranché feront toujours diverger les deux. Ce que vous pouvez enfin affirmer, c'est que *personne d'autre ne commande*. Pour l'écart commande/réalité, la charge utile `mechanism` de la borne porte `chargingEnabled`, `currentA`, `phaseCount` et `pluggedIn` — c'est là qu'il se lit, pas dans `allocatedW`. --- ## 3. Nouvelle clé ARB : `EV_GRID_START` ``` EV_GRID_START { budgetW, floorW, gridW } funding: "grid" ``` **Une borne qui DÉMARRE en soutirant au réseau.** Le surplus ne paie pas son plancher, mais le réglage `acquisitionTolerance` (défaut 0,5) autorise l'appoint : à 0,5, une borne qui exige 4 140 W démarre dès 2 070 W de surplus et tire le reste du réseau. `gridW = floorW − budgetW` est **ce qui est acheté**. C'est le chiffre qui fait de cette ligne une décision, et pas un effet de bord : il mérite d'être montré, pas rangé dans un repli. **Comptabilité — c'est le seul cas où l'`allocatedW` d'une charge se partage** entre les deux compteurs du budget : `budgetW` vient du surplus et entre dans `budget.allocatedW`, `gridW` vient du réseau et entre dans `budget.evReservedW`. `budgetW + gridW == allocatedW`, toujours. Si vous réconciliez la somme des allocations avec le budget, c'est la ligne à traiter à part. **Borné par construction** : le démarrage exige du surplus réel et consomme tout le budget restant, donc **au plus une borne par cycle** peut soutirer. > **Pas encore vu sur machine.** Guetté six cycles au banc, jamais fabriqué : le budget de la > borne est passé de 385 W à 2 350 W sans s'arrêter dans la fenêtre de tolérance. Le motif est > éprouvé par la simulation. Prévoyez la clé, mais ne construisez pas d'écran autour d'une > chose que le terrain n'a pas encore montrée. Seconde clé du même lot, plus rare : `PHASE_LIMIT { limitW, requiredW }` — c'est la **limite de phase** qui interdit, pas le budget. Deux causes, deux gestes : l'une se règle en délestant la maison ou en revoyant l'abonnement, l'autre en attendant le soleil. --- ## 4. Le rang d'une borne est configurable, et il se classe avec les autres `LoadConfig.priority`, comme toute autre charge. **Il n'y a pas de second système de priorité** et il n'y en aura pas : un classement propre aux bornes ne saurait pas exprimer « VE1 > ECS > VE2 », et deux classements qui ne peuvent pas s'interclasser sont un défaut, pas deux fonctionnalités. `ChargingInfo` ne porte donc **aucun** champ de rang. **Le glisser-déposer marche donc sur une seule liste**, bornes comprises, et se pose par `SetLoadConfig` comme le reste. Vérifié sur machine, y compris l'inversion de rang entre deux charges. Le tri est **total** : à rang égal, l'identifiant départage. Laquelle est servie est donc reproductible d'un cycle à l'autre, même avant tout réglage. > **Réserve sur le rang par défaut, et elle vous concerne.** À la création automatique, la borne > reçoit « le plus petit rang existant moins un, borné à 1 ». Quand une charge occupe déjà le > rang 1 — c'est le cas du banc — l'intention « en tête » **dégénère en égalité** : les deux > bornes et le chauffe-eau sont tous à 1. Sans conséquence sur la reproductibilité, mais > **l'ordre affiché à l'installateur avant qu'il n'ait rien réglé n'est pas un choix, c'est un > artefact**. Ne le présentez pas comme un réglage tant que le point n'est pas tranché côté > moteur (`docs/RELEVE_3g2.md` §3.2). Les vrais rangs se posent à la mise en service, par vous. --- ## 5. `targetPercentage` — ta question est tranchée, et elle change ton écran Tu avais remonté que `targetPercentage` est une cible sans mesure d'avancement : aucune classe `evcharger` ne déclare d'état de charge, aucune ne porte d'interface véhicule. **C'est exact** — vérifié sur les cinq classes du banc. **Et le code fait pire que « il manque une mesure ».** `targetPercentage` est comparé à `carBatteryLevel`, lu sur le Thing *voiture*. Or dans le seul cas où ce mécanisme s'active — une voiture manuelle, dont `batteryLevel` est **inscriptible** — c'est **le moteur qui écrit cette valeur** : il intègre la puissance de la borne, applique un facteur de pertes, divise par une capacité déclarée, et réinjecte le résultat dans l'état du Thing. Il compare donc sa cible à sa propre estimation, bâtie sur trois grandeurs dont aucune n'est mesurée. **Ce que ça te coûte aujourd'hui** : l'estimation sort par la frontière RPC sous le nom `batteryLevel`, **indistinguable d'une mesure**. Un écran qui affiche « 62 % » affiche une intégration. Tu mens de bonne foi, et rien dans la charge utile ne te permet de t'en apercevoir. ### Les décisions (Patrick, 2026-08-27) — `specs/spec_loadmodel.md` LM-1009 **D1 — L'avancement s'affiche en ÉNERGIE LIVRÉE, jamais en pourcentage.** « 8,4 kWh livrés depuis le branchement » est vérifiable ; « 62 % » suppose une capacité que personne ne connaît. Le pourcentage garde sa place dans la **saisie de l'intention** — l'utilisateur pense sa voiture en pourcentage — mais **sort de l'affichage de l'avancement**. **D2 — Obligation par SESSION d'abord.** La source est `sessionEnergy`, l'énergie livrée depuis le branchement, publiée par la borne. C'est aussi l'usage réel : on branche le soir pour partir le matin. L'obligation *quotidienne* (`minEnergyWhPerDay`) est un lot à part, qui attend l'historique du §11 — ne l'anticipe pas dans l'écran. **D3 — L'estimation sera ÉTIQUETÉE à la publication.** Tant que le chemin `carBatteryLevel` survit, il ne franchira plus la frontière RPC sous le même nom qu'une mesure. Le précédent existe déjà côté moteur : `estimatedPowerW` porte son préfixe exprès. ### Ce que ça implique pour toi, concrètement - **Deux régimes de vérité, et il faut pouvoir dire lequel s'applique** (condition C2 de la spec) : une obligation en **énergie** peut annoncer son atteinte ; une cible en **pourcentage** publie une intention et un avancement *estimé* — **jamais « tenue »**. Un avancement mesuré et un avancement estimé ne se distinguent par aucune de leurs valeurs : les deux sont un nombre qui monte. Si le régime ne se lit pas, le second se fait passer pour le premier. - **Certaines bornes ne publient pas `sessionEnergy`** (condition C1) — mesuré : `wallboxNoMeter` et l'une des deux classes `wallbox`. Sur celles-là l'avancement n'est pas mesurable, et **l'écran doit le montrer** : une échéance qui cesse silencieusement d'être mesurable est pire qu'une échéance absente, parce qu'elle continue de s'afficher. Surtout, ne pas afficher zéro — « pas mesurable » et « rien livré » sont deux états différents. - **Rien n'est encore codé côté moteur** : `sessionEnergy` n'est lue nulle part et `LoadContextTelemetry::sessionWh` est déclarée mais jamais remplie. Le §10 le fera. Ce brief te donne la forme d'arrivée pour que tu ne bâtisses pas sur le pourcentage entre-temps. --- --- ## 6. Réponses à vos quatre constats du 2026-08-28 ### A — la conclusion est retenue, le mécanisme ne l'est pas **Votre conclusion est juste, elle est adoptée, et elle est LIVRÉE** (`spec_loadmodel.md` **LM-1208-b**, moteur `1.15.2+etm25`) : « pas de voiture assignée » cesse d'être un critère d'exclusion. Vos trois arguments tiennent, et le troisième est décisif — **le critère contredit LM-1206**, qui donne le rang et le plancher à la borne et non au véhicule. **Mais le budget n'était pas faussé, et c'est votre propre banc qui le montre.** Le budget part de la mesure **brute** du compteur (invariant 8) : la consommation d'une borne exclue y est déjà déduite. Journal du 28 à 10:49, pendant que la V2C tirait : ``` Current load on root meter: 3531.4W (avant : −2915 W, en export) [Arbitre] "chauffe-eau" → "Surplus insuffisant (−3531 W) — chauffe-eau à 0 W" ``` **L'ECS n'a pas été servi par-dessus** — l'arbitre a vu l'import et a refusé. L'exclusion est *conservatrice* : la charge exclue retire du budget aux autres, et personne ne le lui rend. > **Votre règle générale est néanmoins retenue**, parce qu'elle a un porteur étroit et réel : une > borne exclue du contexte mais **commandée par le proxy** ne reçoit aucun `evReservedW`. Sa > puissance commandée et pas encore mesurée n'est réservée par personne. Transitoire — un cycle — > et sans effet aujourd'hui, mais **elle mordra au §10**, où la passe éco décide d'acheter. > **Ce qui change pour vous, concrètement** : une borne en mode éco, véhicule branché, **sans > voiture déclarée**, est désormais arbitrée — elle reçoit une enveloppe, un rang, et un motif > publié dans `GetLoadTelemetry` comme n'importe quelle autre charge. Ce qu'elle n'a pas : > l'échéance et la cible d'énergie, qui restent au proxy et supposent un véhicule connu. C'est > une **fonction dégradée**, à montrer comme telle — pas une borne en défaut. > > **Et une conséquence trouvée en implantant, qui vous concerne** (`LM-1208-c`) : sans voiture, > le moteur ne connaît pas la capacité du véhicule, donc il **ne bascule plus les phases** et > retient le `phaseCount` de la borne. Une borne triphasée reste arbitrée en triphasé — c'est > volontaire et conservateur pour le budget. La bascule 1↔3 revient dès qu'une voiture est > déclarée. **⚠️ Un écart de MESURE mérite votre attention, et il est distinct** : le compteur racine lit **3 250 W** quand la V2C annonce **3 980 W** à elle seule (plus la PAC à 800 W). Environ 1,5 kW que le compteur racine ne voit pas. Ce n'est pas un problème d'arbitrage — c'est une question de câblage ou de vue SunSpec, de la même famille que l'aveuglement du compteur aux résistances ECS documenté dans `AGENTS.md`. **À investiguer pour lui-même** : un budget juste suppose un compteur qui voit tout. ### B — même cause, mais le mode manuel n'est pas en cause Corriger A ne suffira pas : la V2C est en **`ChargingModeNormal`**, et le mode manuel reste un critère d'exclusion **délibéré** — l'utilisateur pilote, l'arbitre s'abstient. **Le vrai point est ailleurs, et il vous concerne** : le mode par défaut d'une borne fraîchement détectée **est** `ChargingModeNormal`. Une borne neuve n'est donc jamais arbitrée tant que personne n'a choisi un mode éco. C'est correct par construction, mais **invisible** — et « la borne ne charge pas toute seule » se lira comme un défaut. C'est un geste de mise en service à montrer, pas un correctif moteur. ### C — vous pouvez déjà la retirer, aujourd'hui, sans rien ajouter L'entrée résiduelle de la Terra AC **se supprime depuis l'app** : `SetLoadConfig` **remplace l'ensemble**, donc renvoyer la liste sans cette entrée la retire. Et elle ne sera **pas recréée** : l'auto-provisionnement ne tourne que pour les bornes présentes dans nymea, et le Thing a été supprimé. C'est exactement le « geste, pas une suppression d'office » que vous demandez, et il existe déjà. > ⚠️ **CORRECTION du 2026-08-29 — cette limite était énoncée à l'envers.** Nous vous avions > écrit que supprimer l'entrée d'une borne dont le **Thing existe encore** la ferait **recréer > au cycle suivant**. **C'est faux, et c'est mesuré** : sur `.75`, entrée retirée, Thing vivant, > **rien n'a été recréé en 140 s**. > > La cause : l'auto-provisionnement ne tourne qu'à la **création de l'adaptateur**, pas à chaque > cycle. L'adaptateur est construit depuis le **Thing** — il survit à la suppression de > l'entrée, donc la borne n'est plus jamais proposée à la configuration. > > **Ce que cela produisait** : la borne restait **arbitrée** mais **absente de > `GetLoadConfig`**, à un rang de queue non réglable — le trou que 3g-2 avait refermé, rouvert > par le côté client. > > **CORRIGÉ en `1.15.2+etm28`** (`LM-1209-e`). Le rattrapage est désormais **cyclique** : la > condition est « cette borne n'a pas d'entrée », plus « l'adaptateur vient de naître ». Une > entrée supprimée est donc **recréée au cycle suivant**. > > **Mais elle revient DÉCLASSÉE — et c'est ce qu'il faut montrer à l'installateur.** Elle > réapparaît **en queue**, avec `rankOrigin: "auto"` : personne ne l'a classée, son rang est de > nouveau un défaut. **Le rang qu'il avait posé est perdu avec l'entrée.** > > **Donc la suppression d'une entrée n'est PAS un moyen de retirer une borne de l'arbitrage** — > elle revient, à une autre place. `enabled: false` est la voie, et lui conserve son rang. Si > votre écran propose une suppression, il devrait le dire avant, pas après. ### D — instruction reçue, rien ne bouge côté moteur Aucun cas particulier V2C dans le moteur : le contrat reste celui des autres bornes, et **C1 de LM-1009 est maintenue** — d'autres bornes n'auront ni `sessionEnergy` ni valeur accumulable. Votre distinction est plus fine que ce que le brief avait anticipé et elle est notée : **un champ absent se voit ; un champ qui s'efface se lit comme un zéro.** C'est un troisième régime, entre « mesuré » et « pas mesurable » — et c'est précisément pourquoi C2 exige que le régime soit *publié* et non déduit d'une valeur. Et `targetPercentage = 80` observé sur la V2C **confirme sur machine** le défaut corrigé en `+etm23` : le champ omis retombait à son défaut. Ce n'est plus une déduction. --- ## 7. Le compteur par charge : vous avez raison, et le modèle bouge *(Votre constat d'usage du 2026-08-28. Tranché : `spec_loadmodel.md` **LM-1105/1106**. Contrat : `INTERFACE.md`. **LIVRÉ** en `1.15.2+etm26` — `measuredW` n'existe plus.)* ### Le constat est retenu tel quel Désigner une borne comme **son propre compteur** pour qu'elle affiche ce qu'elle mesure déjà n'est devinable par personne. Une note d'écran qui explique un réglage est le signe que le défaut est dans le **modèle**, pas dans l'app — et c'est de la configuration demandée à l'installateur pour compenser ce que le moteur peut déduire. **Le moteur lira l'appareil qu'il pilote** dès qu'il porte lui-même l'état de puissance. Une précision de rédaction : la règle porte sur le **Thing**, pas sur le mécanisme. « Les bornes mesurent, les relais non » est vrai en pratique et faux en droit — certains `powerswitch` portent `currentPower`, et `RelayRouter` sait déjà les sommer. ### Vos deux points, tranchés **1. Le compteur explicite reste prioritaire — oui, et c'est écrit.** Mais la raison principale n'est pas la précision : c'est qu'**un réglage sans effet est pire que pas de réglage**. Si le moteur préférait l'appareil au compteur que l'installateur a pris la peine de désigner, le champ deviendrait décoratif. La priorité porte sur la **source**, pas sur la valeur : compteur déclaré mais Thing absent → on retombe sur l'appareil, et la charge utile **dit lequel a servi**. **2. Votre second point est le plus important, et il décide de la FORME.** En le vérifiant, on a trouvé que le moteur porte **déjà** trois régimes sous un seul nom — et qu'il jette la distinction : une borne rend une mesure ; un `relay-router` rend une mesure **ou le palier nominal** selon que ses relais mesurent (le booléen est calculé, puis perdu) ; un `sg-ready` rend **toujours** une puissance déclarée, jamais mesurée. Donc le régime **se publie**, il ne se déduit pas d'une valeur — c'est votre C2 de LM-1009, mot pour mot. `measuredW` disparaît au profit de : ```jsonc "measurement": { "source": "meter" | "device" | "none", "powerW": 3980.0 } ``` `powerW` est **absent** sous `none`. Et `none` est un état **positif** : « cette charge n'est pas mesurable ». C'est lui qui vous permet de **masquer** le bloc de mesure au lieu d'afficher un vide sur un `sg-ready` qui ne le sera jamais — et de distinguer « pas mesurable » de « compteur en panne », que rien ne séparait. ### Ce que la source change pour vos écrans de diagnostic | `source` | Ce qu'un écart commandé↔mesuré autorise à dire | |---|---| | `device` | **La charge n'obéit pas** — véhicule qui refuse, thermostat ouvert, plafond interne | | `meter` | **La charge, le compteur, ou le câblage** — trois hypothèses, l'écart n'en tranche aucune | | `none` | **Rien.** Pas d'écart affichable, et surtout pas un « 0 W d'écart » | > Une asymétrie qui va plus loin que « le compteur mesure mieux », et qui vaut d'être dite à > l'écran : sous `device`, une **concordance** ne prouve rien — plusieurs wallbox renvoient leur > consigne en guise de puissance. Sous `meter`, elle est une **preuve**. `device` détecte la > désobéissance ; `meter` est le seul à pouvoir détecter un **mensonge**. C'est pourquoi > `meterThingId` reste **autorisé** sur une borne qui publie déjà : à masquer par défaut, pas à > interdire. **Et une mise en garde sur la façon d'écrire cette règle chez vous : décidez sur `measurement.source`, jamais sur `adapter`.** « Masqué dès que la charge mesure, sauf SG-Ready » paraît équivalent et ne l'est pas — un `relay-router` à topologie **mixte** publie lui aussi `none`, et vous masqueriez le champ sur un ECS qui ne peut être mesuré **que** par un compteur dédié. La règle sûre tient en une ligne, et elle couvre l'exception SG-Ready sans que vous ayez à la connaître : ``` si measurement.source == "device" → compteur dédié en réglage avancé (replié) sinon → compteur dédié proposé, visible ``` C'est toute la raison d'être de `source` : le régime est **publié** pour ne pas être déduit. Le redéduire du mécanisme côté app remettrait chez vous la déduction qu'on vient de retirer du moteur — et vous la maintiendriez à chaque mécanisme ajouté. ### Deux choses trouvées en implantant, qui changent ce que vous afficherez **`sg-ready` est `none` par CONSTRUCTION, pas par accident de matériel.** Un contact SG-Ready est un contact de **signal** : la PAC est alimentée ailleurs, son courant ne traverse jamais K1/K2. Même montée sur des relais qui mesurent — c'est le cas de notre banc — la charge reste non mesurable par son organe de commande. Pour une PAC, `meterThingId` n'est pas un réglage avancé : c'est la **seule** voie vers une mesure, et l'écran devrait le proposer, pas le masquer. **Une topologie de relais MIXTE est `none` aussi.** Un relais mesurant et deux nus donneraient une somme partielle — 500 W annoncés là où 1 500 W coulent — et l'écart vous ferait afficher une panne qui n'existe pas. Une mesure partielle n'est pas une mesure. > Et un point d'affichage qui vous évitera un faux bug : **`{source: "device", powerW: 0}` est > une mesure**, pas une absence. Un routeur de relais au palier 0 mesure réellement 0 W et reste > mesurable — la source ne bascule pas vers `none` quand la charge s'éteint. Si elle basculait, > votre bloc de mesure apparaîtrait et disparaîtrait au rythme du thermostat. ### Votre coût, et il est d'un écran `measuredW: X` → `measurement: {source, powerW: X}` ; son **absence** → `source: "none"`. La clé n'est pas doublée pendant une transition, délibérément : deux noms pour une chose divergent (c'est la règle qui a fait naître `renderFr`). Prévenez-nous si le calendrier de l'app impose l'inverse. --- ## 8. Le rang d'une charge créée d'office — un champ de plus, et un piège d'écriture *(Décision Patrick, 2026-08-28 : `spec_loadmodel.md` **LM-1209**. Contrat : `INTERFACE.md`. Livré en `1.15.2+etm27`.)* **Ce qui change dans le moteur** : une borne détectée entre désormais **en queue** de la liste, plus en tête. Elle ne passe devant aucune charge que quelqu'un a classée — une détection ne réordonne pas ce qu'un humain a décidé. Corollaire pratique pour vous : **le rang produit est toujours libre**, et `priority` est maintenant **unique**, garanti par le contrat au lieu d'être supposé. > ⚠️ **`SetLoadConfig` REFUSE désormais en bloc un ensemble à rangs doublés.** Si votre écran de > réordonnancement peut produire deux charges au même rang — ne serait-ce qu'un instant, pendant > un glisser-déposer — l'écriture entière sera rejetée. À vérifier chez vous. > > Une configuration **héritée** à rangs doublés, elle, n'est pas amputée : elle est conservée au > chargement et signalée au journal. Votre première écriture la répare. **Le champ nouveau : `o:rankOrigin`.** `"auto"` dit « cette entrée a été créée d'office et personne n'a jamais classé cette charge » — c'est ce qui vous permet d'afficher **« rang par défaut »** au lieu de laisser croire à un choix. `"user"` dit que le rang est assumé. **Absent n'affirme rien**, et c'est volontaire : une configuration héritée ne prétend pas que ses rangs ont été décidés. N'affichez le badge que sur un `"auto"` **explicite**. > C'est le même motif de conception que `measurement.source`, un cran plus loin : **publier le > régime, et ne jamais laisser un silence passer pour une affirmation.** Une énumération plutôt > qu'un booléen précisément pour ça — `rankIsDefault: false` affirmerait que le rang a été > choisi, alors que c'est ce qu'on ignore. ### Le piège d'écriture, et c'est le point à retenir `SetLoadConfig` **remplace en bloc**. Quand l'installateur déplace la charge B, vous renvoyez aussi l'entrée de la charge A — et A n'a toujours été classée par personne. **Le moteur protège ce cas** : la marque tombe quand **le rang de cette charge-là** change, jamais parce qu'une écriture est arrivée. Vous pouvez donc omettre la clé sans rien casser — l'omission **ne lève rien**. Deux conséquences pour vous : - **Pour lever la marque sans déplacer la charge** — « j'ai vu ce défaut, il me va » — envoyez `"user"` explicitement. C'est la seule voie. - **N'écrivez jamais `"auto"`.** Le moteur refuse l'écriture entière : ce serait affirmer un geste qu'il n'a pas fait. Renvoyer verbatim une entrée qui le porte déjà reste licite — l'aller-retour `Get` → `Set` n'est pas cassé. > **Vérifié sur `.75` le 2026-08-29, sur une marque FRAÎCHE** — entrée recréée au redémarrage, > rang 5, `rankOrigin: "auto"`, **rien déplacé avant l'essai**. **Vous pouvez renvoyer le `"auto"` sans le filtrer** : l'écho > verbatim intégral passe, et l'écho verbatim **avec un renommage ailleurs** passe aussi — > `EnergyErrorNoError`, la marque survit, aucun rang ne bouge. Un renommage n'est donc **jamais** > bloqué par la présence d'une borne auto-provisionnée. Votre filtre reste inoffensif ; il n'est > simplement pas nécessaire. > > **Le seul refus qui vous concerne est l'ÉCHO PÉRIMÉ**, et il est mesuré : si vous déplacez > l'entrée (la marque tombe), puis renvoyez un payload mis en cache **avant** ce déplacement — > lequel porte encore `"auto"` —, l'écriture est rejetée. C'est correct : ce payload affirme > quelque chose qui n'est plus vrai. **Le remède est de relire avant de réécrire**, et le même > geste passe ensuite. Un cache de configuration survivant à une écriture est le vrai piège, > pas le champ. > > **C'est ce qui explique nos deux relevés contradictoires.** Notre premier essai suivait un > déplacement fait pour borner le refus — la marque était donc tombée, et votre écriture portait > un `"auto"` que le persisté ne portait plus : refus correct, cas de l'écho périmé. Sur marque > fraîche, sans rien déplacer, l'écho passe. **`sansRankOriginAuto()` peut partir** ; ce qu'il > faut à la place est de **relire avant de réécrire**. ### Le regroupement par domaine : à l'affichage, jamais à l'écriture L'insertion en queue entrelace les domaines (`ecs, ev, ev, heating, ev`) et votre note d'entrelacement s'allume à juste titre — mais la conclusion est l'inverse de celle qui vient naturellement. Tranché en `spec_loadmodel.md` **LM-1209-d**. La contiguïté des domaines **n'a jamais été un invariant** : le waterfall trie sur le rang seul, et `domain` est une métadonnée que l'arbitre ne lit pas. L'exiger du moteur serait d'ailleurs contradictoire avec la raison même de LM-1209 — insérer une charge `heating` en respectant la contiguïté décalerait toutes les `ev` suivantes, soit une détection qui réordonne une installation qu'un humain a classée. **Le même raisonnement vaut chez vous.** Un regroupement appliqué **à l'enregistrement** est une machine qui réordonne ce qu'un humain a décidé, au pire moment : il défait un entrelacement délibéré — « l'ECS après la borne 1 mais avant la borne 2 » est un arbitrage légitime, et le §10 en produira d'autres — et remonterait au rang 3 une borne que le moteur vient de ranger en queue. - **Grouper à l'affichage : oui.** Rien ne s'y oppose. - **Grouper à l'écriture : non.** Si le regroupement a de la valeur, qu'il soit un **geste explicite** de l'installateur, visible et réversible — jamais une normalisation au moment de sauver. - **La note d'entrelacement ne devrait pas être un avertissement.** Une liste entrelacée n'a rien d'incohérent ; l'annoncer comme un défaut pousse à « réparer » un ordre que personne n'a cassé. Au plus une proposition. > **Une ligne pour votre README des sondes** : **un refus arrive en `status: "success"`**, le > verdict étant dans `params.energyError`. Une sonde qui teste `status` annonce « accepté » sur > une écriture rejetée. C'est désormais en tête d'`INTERFACE.md`, avec l'extrait qui va bien. --- ## Ce qui n'a PAS changé, et qu'il ne faut pas déduire - **L'échéance de départ et le tarif dynamique restent au proxy**, financés au réseau (`funding: "grid"`, motifs `EV_DEADLINE` / `EV_SPOT_MARKET`). Leur puissance vit dans `budget.evReservedW`, pas dans `budget.allocatedW`. - **`chargingState` de `ChargingInfo`** garde son sens et reste publié par l'arbitre pour les bornes qu'il commande. - **`SetChargingInfo` n'est toujours pas partiel** — l'objet remplace le stocké, et un champ writable omis retombe sur son défaut. La description publiée le dit désormais, et `INTERFACE.md` en donne la table. Lire → modifier → renvoyer complet. - **`repeatDays` va de 1 (lundi) à 7 (dimanche)**, pas de 0 à 6. La documentation disait le contraire ; le code a toujours refusé le 0.