# 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. > Quatre points. Le deuxième **retire une mise en garde** que l'app portait, et il est le plus > intéressant pour vous : du code conditionnel peut disparaître. --- ## 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. --- ## 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.