/bug-report était un stub. Un champ libre seul produit « ça ne marche pas », et l'aller-retour pour obtenir versions, charges et état d'arbitrage coûte des jours. Tout ce que l'app SAIT est donc joint d'office : version du paquet, hôte et état de connexion, ligne de versions du schéma, les charges déclarées avec adaptateur / rang / domaine, et le dernier cycle publié. Deux choix qui ne sont pas cosmétiques : - LES DEUX IDENTITÉS DU BUDGET sont dans le rapport, avec leur verdict. Elles valent souvent plus que la description du symptôme : portées fausses, elles désignent le moteur ; portées vraies, elles désignent l'écran. Sans elles, le premier échange consiste à les demander. - Aucun secret n'y entre : ni PIN installateur même haché, ni jeton, ni mot de passe. Un rapport voyage — courriel, capture, fil de discussion — et ce qui y entre en sort. Les UUID de Things restent : ce sont eux qui permettent de recouper avec le journal de la box, et ils n'ont aucune valeur hors de l'installation. Les absences gardent leur sens, comme partout ailleurs : « aucune télémétrie reçue » n'est pas un arbitrage à zéro, « budget ABSENT » n'est pas un budget de zéro watt, et un financement omis se dit omis. Pas d'envoi automatique : il n'existe aucun service de collecte, et un bouton « Envoyer » sans destinataire donnerait le sentiment que quelqu'un l'a reçu. Le rapport se copie, ce qui est vérifiable. package_info_plus est ajouté plutôt qu'une constante de version recopiée à la main : une constante dérive de pubspec.yaml en silence, et un rapport qui annonce la mauvaise version envoie chercher un défaut dans un code qui n'est pas celui-là. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
12 KiB
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é :wallboxNoMeteret l'une des deux classeswallbox. 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 :
sessionEnergyn'est lue nulle part etLoadContextTelemetry::sessionWhest 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", motifsEV_DEADLINE/EV_SPOT_MARKET). Leur puissance vit dansbudget.evReservedW, pas dansbudget.allocatedW. chargingStatedeChargingInfogarde son sens et reste publié par l'arbitre pour les bornes qu'il commande.SetChargingInfon'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, etINTERFACE.mden donne la table. Lire → modifier → renvoyer complet.repeatDaysva de 1 (lundi) à 7 (dimanche), pas de 0 à 6. La documentation disait le contraire ; le code a toujours refusé le 0.