LM-1208-b appliqué : `assignedCarId` sort du filtre d'entrée du waterfall. Restent deux exclusions, et elles ne disent pas la même chose — le mode manuel est une INTENTION (l'utilisateur pilote), l'absence de véhicule branché un CONSTAT (rien à consommer de ce qu'on allouerait). Le critère venait du proxy, où il a un sens : sans voiture, pas de SoC, pas de capacité, pas d'échéance à planifier. Le waterfall n'a besoin d'aucune des trois pour allouer du surplus. Il contredisait LM-1206 — le rang et le plancher appartiennent à la borne, pas au véhicule — il n'était pas vérifiable, et il excluait la borne d'un invité au moment précis où elle consomme. Ce qu'on perd sans voiture est une FONCTION, échéance et cible d'énergie, pas l'arbitrage. LM-1208-c, trouvé en implantant, et c'est lui qui aurait mordu : retirer le critère ne suffit pas. Le proxy ne PRÉPARE pas une borne sans voiture — prepareInformation() la filtre en entrée — donc elle entre dans le waterfall sans ChargingProcessInfo. Or internalProcessInfo() rend un objet par DÉFAUT, et ses valeurs sont plausibles : une phase, pas de bascule. Une borne triphasée aurait été annoncée monophasée et le budget divisé par trois — plafond 3 680 W au lieu de 11 040, 16 A commandés au lieu de 9, 11 kW tirés d'une enveloppe qui en comptait 3,7. Le défaut de 3g-1 pris à l'envers, et silencieux. D'où internalHasProcessInfo() : la question « cette borne a-t-elle été préparée ce cycle » devient POSABLE au lieu d'être déduite d'une valeur crédible. Sans ChargingProcessInfo on ne bascule pas — basculer suppose de savoir ce que la voiture accepte — et on retient le phaseCount de la BORNE, hypothèse conservatrice au sens de LM-104 : elle surestime la consommation. Éprouvé par testChargerWithoutCarIsArbitrated, et VÉRIFIÉ ÉCHOUANT sans le garde-fou : 3 680 W au lieu de 6 000 W alloués. Le test porte les deux moitiés — la borne sans voiture est servie ET le mode manuel reste une exclusion, sans quoi il passerait aussi si toute borne était arbitrée sans condition. amd64 0 erreur / 0 avertissement. Suites, un processus par test : simulation 29/29 (dont run 16/16), charging 16/16, loadmodel 18/18, spotmarket 7/7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
282 lines
17 KiB
Markdown
282 lines
17 KiB
Markdown
# 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.
|
||
|
||
> Six points. Le sixième répond à vos quatre constats du 28 août.
|
||
> 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à.
|
||
|
||
> **La limite à connaître** : si vous supprimez l'entrée d'une borne dont le **Thing existe
|
||
> encore**, elle sera recréée au cycle suivant — c'est l'auto-provisionnement, et c'est voulu.
|
||
> Retirer une borne de l'arbitrage sans supprimer son Thing se fait par `enabled: false`, pas par
|
||
> suppression.
|
||
|
||
### 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.
|
||
|
||
---
|
||
|
||
## 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.
|