LM-1209-e. Le provisionnement vivait dans la branche « l'adaptateur vient d'être créé » de syncAdapters(). Or l'adaptateur est construit depuis le THING : il survit à la suppression de l'entrée de configuration. Une entrée supprimée depuis l'app n'était donc jamais recréée — mesuré sur .75, rien en 140 s. Le trou de 3g-2 rouvert par le côté client : la borne restait ARBITRÉE mais ABSENTE de GetLoadConfig, à un rang de queue non réglable. Elle consommait du budget sans que personne puisse la classer, et rien ne le disait. La condition devient un ÉTAT et non un ÉVÉNEMENT — « cette borne n'a pas d'entrée » au lieu de « l'adaptateur vient de naître ». C'est la leçon qui dépasse le défaut : un rattrapage dont la condition est un événement ne rattrape rien, puisqu'il ne se rejoue pas. Même distinction qu'entre ECS-411, qui relit l'état réel des relais, et l'hypothèse de départ qu'il a remplacée. L'entrée recréée revient en rankOrigin "auto" — personne ne l'a classée, son rang est de nouveau un défaut — et le rang posé par l'installateur est PERDU avec l'entrée. Supprimer n'est donc pas un moyen de retirer une borne de l'arbitrage : elle revient, déclassée, en queue et avec son badge. C'est exactement pourquoi la marque doit se voir. enabled: false reste la voie et conserve le rang. Le refus de provisionnement n'est annoncé qu'une fois par borne (m_provisionRefuse) : le rattrapage étant cyclique, le répéter produirait un avertissement par minute et pour toujours — la règle est déjà posée pour les charges sans décision dans buildTelemetry(). Le drapeau tombe au premier succès. testDeletedEvChargerConfigComesBack couvre les deux moitiés : le rattrapage, et le cas négatif — il ne se rejoue PAS quand l'entrée existe, sans quoi il réécrirait la configuration à chaque cycle, donc un changed(), donc un rebuild d'adaptateurs, donc des verrous réarmés en boucle (ECS-412). Vérifié échouant avec l'ancien comportement. Le brief §6-C, qui promettait à l'app l'inverse de ce qui se passait sur le seul geste client qui détruit de la configuration, est corrigé — et dit désormais ce que la suppression coûte vraiment : le rang. Passe aussi LM-1209-d (grouper à l'affichage, jamais à l'écriture) et l'avertissement de sonde en tête d'INTERFACE.md. amd64 0/0. simulation 32/32, charging 16/16, loadmodel 20/20, spotmarket 7/7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
500 lines
30 KiB
Markdown
500 lines
30 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.
|
||
|
||
> 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.
|