# 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. > Cinq points. 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. --- ## 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.