L'app a validé le préalable sans réserve, et sa formulation de pourquoi counts n'a pas été livré mérite d'être gardée : une identité dépendante du temps est pire que pas d'identité. Vraie au premier cycle après un changement, fausse après convergence, elle passe tous les tests — qui portent sur des cycles isolés — et certifie un total faux chez les clients, avec le sceau d'une identité publiée. draw.committedW est publié dans la MÊME TRAME que budget : les deux décrivent le même cycle, et les lire à deux instants ferait comparer deux mondes. SEUL, et c'est la deuxième demande, celle qui compte. authorisedW, remainingW, binding, perPhaseBound sont ABSENTS, jamais à 0 — un authorisedW: 0 assorti d'un binding par défaut afficherait un plafond de soutirage qui n'existe pas, avec sa source. Un chiffre crédible et faux, c'est-à-dire la faute même que l'objet doit rendre impossible. budget.evReservedW n'a plus qu'UNE nature. Il recevait la part réseau d'EV_GRID_START — une vraie tranche d'allocation — en plus de la correction A. Déménagée dans draw.committedW. Sans ce déplacement, aucune définition du champ n'aurait été vraie pour ses deux termes, et l'app aurait renommé son libellé sur une définition à moitié fausse. Sa définition est écrite, puisqu'elle affichait « Réservé au véhicule » : c'est une CORRECTION du budget de surplus, pas une réservation ; elle tend vers 0 à mesure que la mesure rattrape la commande, sans qu'aucune décision n'ait changé. Le contrat dit : ne l'additionner à rien, il ne se réconcilie jamais. Le libellé affirme deux choses fausses — que c'est réservé, et que ça concerne le véhicule plutôt que la latence de sa mesure. EV_GRID_START cesse d'être une exception : ses deux moitiés tombent dans des compteurs de même nature. Le partage demeure — c'est un fait, une part est achetée — mais il ne s'énonce plus comme une entorse. Publication seule, aucune arithmétique de budget ne change : vérifié avant d'écrire que la ligne alimentant evReservedW depuis la cascade n'avait aucun retour dans le calcul. simulation 33/33, charging 16/16, loadmodel 20/20, amd64 0/0, doxygen 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
841 lines
50 KiB
Markdown
841 lines
50 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.
|
||
|
||
> Dix points. Le neuvième était une question ; le dixième répond à la maquette qui y a
|
||
> répondu — et rectifie deux choses, dont une qui change un écran. 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.
|
||
|
||
---
|
||
|
||
## 9. UNE QUESTION POUR VOUS — l'ordre de service apparent, quand le §10 arrivera
|
||
|
||
*(Point 8.5 de `docs/DESIGN_10.md`, dernier point ouvert du lot. `spec_loadmodel.md` LM-1002 et
|
||
LM-1006. **Rien à câbler** : le §10 n'est pas implémenté, et il ne le sera pas avant que cette
|
||
question soit tranchée — c'est une condition de livraison, pas une suite.)*
|
||
|
||
**Ce n'est pas à nous d'y répondre.** Ce que nous vous demandons est du même ordre que la
|
||
maquette des mécanismes pour les six écrans : **dire quelle forme de publication rendrait
|
||
l'écran possible.** Nous fabriquerons ensuite la charge utile qui la sert.
|
||
|
||
### Le fait qui produit le problème
|
||
|
||
Le §10 donne deux niveaux de besoin à une charge : **éco** (le minimum vital — un ballon doit
|
||
voir 50 °C, une PAC ne doit pas geler) et **confort** (le reste). L'arbitre fera **deux passes
|
||
sur la MÊME liste, dans le MÊME ordre** :
|
||
|
||
```
|
||
passe 1 — ÉCO : parcourir les charges par rang croissant, servir leur plancher éco
|
||
passe 2 — CONFORT : reparcourir la MÊME liste, dans le MÊME ordre, avec le reliquat
|
||
```
|
||
|
||
Le rang garde un sens unique — l'ordre de service posé par l'installateur. La hiérarchie
|
||
éco/confort le **surplombe** : elle dit *quel besoin* passe avant, jamais *quelle charge*.
|
||
|
||
**Et voici la conséquence, qui est voulue et qui aura l'air d'un bug** :
|
||
|
||
> **le plancher éco d'une charge de rang 4 est servi AVANT le confort d'une charge de rang 1.**
|
||
|
||
Formellement c'est un ordre lexicographique sur `(niveau, rang)` — total, dérivé du rang
|
||
existant, pas un second système de priorité. Vu de l'écran, c'est un installateur qui vient de
|
||
poser son ordre et voit le moteur ne pas le suivre. **Sans un écran qui montre les deux passes,
|
||
le premier retour terrain sera « l'ordre n'est pas respecté ».**
|
||
|
||
### Les trois choses à trancher
|
||
|
||
**1. Sur quoi la liste est-elle ordonnée ?** Les deux réponses coûtent quelque chose, et il faut
|
||
choisir laquelle :
|
||
- **par RANG** — l'ordre affiché n'est alors plus l'ordre de service, et il faut le **dire** à
|
||
l'écran ; sinon la liste ment par omission ;
|
||
- **par ORDRE DE SERVICE effectif** — le rang cesse d'être lisible dans la liste, alors que
|
||
c'est le seul champ que l'installateur règle.
|
||
|
||
**2. Une charge apparaît-elle une fois ou deux ?** Une entrée par passe — la même charge à deux
|
||
endroits de la liste — ou **une seule entrée portant deux états** ?
|
||
|
||
**3. Que voit-on d'une charge dont l'éco est servi et le confort non ?** C'est le cas **le plus
|
||
fréquent** en régime normal, et **il n'a aucun équivalent aujourd'hui** : la télémétrie actuelle
|
||
publie une allocation et un motif par charge, un seul de chaque. Ni « servie à moitié », ni
|
||
« servie pour son minimum, en attente pour le reste » ne sont exprimables.
|
||
|
||
### Ce dont vous disposez pour décider
|
||
|
||
Aujourd'hui `GetLoadTelemetry` publie **par charge** : `allocatedW`, `funding`, `available`,
|
||
`decision {code, params}`, `measurement {source, powerW}`, `mechanism`, `lock`. **Un seul motif,
|
||
une seule allocation.** Tout ce qui distinguerait les deux passes reste à inventer — un second
|
||
couple, un niveau sur le motif, une entrée dédoublée : **c'est précisément ce que nous vous
|
||
demandons de nommer**, en partant de l'écran et non de la charge utile.
|
||
|
||
> **Deux garde-fous, pour que la réponse ne se ferme pas sur un détail** :
|
||
> - **Une charge reçoit UNE SEULE commande par cycle** (LM-1003), quelles que soient les passes.
|
||
> Deux passes ne veulent pas dire deux consignes ; si l'écran montre deux lignes, elles
|
||
> décrivent une seule décision.
|
||
> - **Le rang reste UN**, et il n'y aura jamais de `prioritéEco`/`prioritéConfort` (LM-1002,
|
||
> rejet explicite) : deux classements à tenir cohérents dans la tête d'un installateur, c'est
|
||
> la chose que le modèle refuse.
|
||
|
||
---
|
||
|
||
## 10. Réponse à votre maquette — le §10 est débloqué, et deux points à rectifier
|
||
|
||
*(Votre `deux_passes_eco_confort_mockup.html`, lue en entier. Décisions inscrites en
|
||
`spec_loadmodel.md` **LM-1010** ; précédence des motifs et réponse à R4 en `DESIGN_10.md`.)*
|
||
|
||
**Vos trois réponses sont retenues telles quelles**, et votre argument sur la liste est meilleur
|
||
que le nôtre : nous disions « le rang doit rester lisible », vous dites que **l'ordre de service
|
||
n'est pas une propriété de la configuration** — il change d'un cycle à l'autre selon les
|
||
planchers atteints. Une liste triée par lui aurait la colonne vertébrale qui bouge sous le doigt.
|
||
Ce n'est pas le rang qui devenait illisible, c'était la **liste** qui devenait instable. La
|
||
formulation est reprise dans la spec.
|
||
|
||
Vos trois refus sont acquis — pas d'index d'ordre de service, pas de second rang, pas de
|
||
pourcentage. Le premier est le plus important pour nous : **une seconde source de vérité pour une
|
||
donnée déductible finit toujours par diverger.**
|
||
|
||
### Votre question de R4 : les compteurs de budget comptent le DÉCIDÉ
|
||
|
||
`budget.allocatedW` et ses voisins comptent ce que la **cascade a décidé**, jamais ce que le
|
||
matériel a reçu. Le budget est le **registre de la cascade** : ce qui détermine ce que reçoit la
|
||
charge suivante est la valeur décidée. L'arrondi vit **après**, dans l'adaptateur, au dispatch, et
|
||
il est propre à chaque mécanisme — compter le commandé obligerait à **rouvrir le budget après le
|
||
dispatch**, c'est-à-dire à décider deux fois.
|
||
|
||
### ⚠️ Deux rectifications, dont une qui change votre écran
|
||
|
||
**1. `allocatedW` n'est PAS « ce qui a été commandé ».** R4 le pose ainsi ; c'est faux
|
||
aujourd'hui. `buildTelemetry()` publie le plan, et `applyActionsToAdapters()` reçoit le slot
|
||
**`const`** : rien n'est réécrit après le dispatch. C'est mesurable —
|
||
`testUnpluggedChargerTakesNoBudget` attend `allocatedW == 3000` sur une borne dont le courant
|
||
commandé donne 2 990 W.
|
||
|
||
Conséquence pour vous : **`Σ targetW == allocatedW` tiendra exactement**, les deux étant du
|
||
décidé. L'écart que votre carte veut nommer — « 1 400 W décidés → palier 1 500 W » — n'est pas
|
||
entre `targetW` et `allocatedW`. Il est entre **`allocatedW` (décidé)** et la **charge utile de
|
||
mécanisme (appliqué)** : `mechanism.stageW`, `mechanism.setpointW`, `mechanism.currentA`. **Vous
|
||
avez déjà les deux nombres aujourd'hui** — ce sont leurs noms qui étaient inversés.
|
||
|
||
**2. La précédence que vous demandiez est écrite**, et `DESIGN_10.md` en porte le tableau :
|
||
`DRAW_CAP` = l'autorisation est épuisée, le geste est de revoir le plafond ;
|
||
`BELOW_MIN_POWER` = il en reste mais la charge ne sait pas la prendre, le geste est de lire la
|
||
fiche technique. Votre lecture du point de rupture est confirmée : une charge refusée **après**
|
||
lui pour cause de palier ne le déplace pas.
|
||
|
||
### R2 est LIVRÉE — `1.15.2+etm30`
|
||
|
||
`funding` vit dans `levels[]`. **Un seul niveau aujourd'hui, et c'est `"comfort"`** : la passe
|
||
unique d'aujourd'hui est celle qui, au §10, servira le confort depuis le surplus — la passe éco
|
||
est la nouveauté du lot, pas la passe existante rebaptisée.
|
||
|
||
```jsonc
|
||
"allocatedW": 3500,
|
||
"levels": [ { "level": "comfort", "targetW": 3500, "funding": "surplus" } ]
|
||
```
|
||
|
||
**`funding` a DISPARU du niveau de la charge, sans résumé.** Le cas mixte est justement celui qui
|
||
le rendrait indécidable : « mixte » serait un troisième code que personne n'a demandé, « grid si
|
||
l'un l'est » une convention que chaque client redériverait autrement. Le motif `decision`, lui,
|
||
reste — parce qu'on saura dire de quel niveau il parle (R7). **On garde un champ hérité quand on
|
||
peut en dire quelque chose de vrai, jamais par symétrie.**
|
||
|
||
**Et l'identité de réconciliation est écrite dans `INTERFACE.md`**, pour que vous n'ayez pas à la
|
||
reconstruire :
|
||
|
||
```
|
||
Σ charges Σ niveaux dont funding == "surplus" → niveau.targetW == budget.allocatedW
|
||
```
|
||
|
||
Avec **l'unique exception, publiée** : `EV_GRID_START` partage une allocation entre deux
|
||
compteurs, et sa contribution au surplus est `decision.params.budgetW`, pas `targetW`. C'est ce
|
||
que **R3** généralisera ; d'ici là l'exception est dans le contrat, pas dans la tête du lecteur.
|
||
|
||
> `levels[]` **absent** = box antérieure à `+etm30`, **ou mode dégradé** — sans plan, il n'y a
|
||
> pas de financement. Votre repli sur `allocatedW`/`decision` reste donc juste, et il couvre les
|
||
> deux cas.
|
||
>
|
||
> **`+etm30` est posé sur `.75`** : `levels[]` y est en ligne, `entry.funding` a disparu, et
|
||
> l'identité a été vérifiée sur la vraie box — Σ targetW(surplus) = 5 000 = `budget.allocatedW`.
|
||
> C'est donc votre branche neuve qui s'exécute désormais, plus le repli.
|
||
|
||
### LES TROIS NOMBRES — porté au contrat, parce que votre R4 en confondait deux
|
||
|
||
Votre correction a fait apparaître un troisième nombre que ni la maquette ni nous n'avions
|
||
nommé. `INTERFACE.md` porte désormais le tableau :
|
||
|
||
| | Où | Ce qu'il dit |
|
||
|---|---|---|
|
||
| **DÉCIDÉ** | `allocatedW`, `levels[].targetW` | ce que la cascade a alloué |
|
||
| **APPLIQUÉ** | `mechanism.stageW` · `setpointW` · `state` · `currentA` | ce qui a été commandé au matériel |
|
||
| **MESURÉ** | `measurement.powerW` + sa `source` | ce qu'un appareil ou un compteur a vu |
|
||
|
||
Et surtout **ce que chaque écart signifie**, parce que c'est là qu'un écran se trompe de
|
||
diagnostic : *décidé ≠ appliqué* est une affaire de **format** — le mécanisme ne sait pas prendre
|
||
n'importe quelle valeur ; *appliqué ≠ mesuré* est une affaire d'**obéissance**, et ce qu'elle
|
||
autorise à conclure dépend encore de `measurement.source` (sous `device` elle accuse la charge,
|
||
sous `meter` elle ouvre trois hypothèses, sous `none` elle n'est pas calculable).
|
||
|
||
Votre carte « 1 400 W décidés → palier 1 500 W » lit donc le **premier** écart, entre
|
||
`allocatedW` et `mechanism.*` — pas entre `targetW` et `allocatedW`, qui sont égaux par
|
||
construction.
|
||
|
||
### R3 : ce qu'il nous faudra de vous
|
||
|
||
Nous la prenons ensuite, et `EV_GRID_START` est le cas qui la force. Notre intention, à
|
||
critiquer avant qu'on l'écrive : **`funding` dit d'où viennent les watts, il ne dit pas dans
|
||
quel compteur ils tombent** — ce sont deux questions, et R3 demande la seconde. Un niveau
|
||
publierait donc sa ventilation :
|
||
|
||
```jsonc
|
||
{ "level": "comfort", "targetW": 1380, "funding": "grid",
|
||
"counts": { "budget.allocatedW": 900, "budget.evReservedW": 480 } }
|
||
```
|
||
|
||
`Σ counts == targetW`, toujours. L'identité devient calculable **sans exception**, et elle vaudra
|
||
telle quelle quand `draw.committedW` arrivera.
|
||
|
||
**`counts` sera publié TOUJOURS, même à une seule destination.** Sur une charge ordinaire,
|
||
`counts: {"budget.allocatedW": 3500}` est redondant avec `targetW` — et c'est le prix à payer.
|
||
L'omettre vous obligerait à connaître la **destination par défaut**, donc à la déduire ; vous
|
||
auriez alors deux chemins de lecture, dont un qui n'est exercé que sur les charges ordinaires,
|
||
c'est-à-dire presque toutes. Et l'identité deviendrait conditionnelle (« si `counts`, sommer ;
|
||
sinon attribuer `targetW` au compteur par défaut ») — une identité à branche est une identité
|
||
qu'on peut se tromper à écrire, en silence.
|
||
|
||
> C'est la règle qui a payé toute la semaine : **une absence ne doit jamais vouloir dire « le
|
||
> défaut s'applique »**. `measurement.source: "none"` est publié plutôt que la clé retirée ;
|
||
> `rankOrigin` absent n'affirme rien, surtout pas `"user"` ; `levels[]` absent dit « box
|
||
> antérieure », pas « un seul niveau implicite ». Le silence ne porte que l'ignorance.
|
||
|
||
### Et une question que `counts` fait apparaître, sur `funding` lui-même
|
||
|
||
En posant la ventilation, on voit que **R2 a déplacé le problème d'un cran sans l'éliminer pour
|
||
`EV_GRID_START`** : ce niveau-là est financé **en partie au surplus, en partie au réseau**.
|
||
`funding: "grid"` y est donc déjà une approximation — au niveau, comme il l'était à la charge.
|
||
|
||
Nous ne touchons pas à `funding` sans vous : c'est votre champ de réconciliation. Mais il faut
|
||
choisir en connaissance de cause, et les deux ne dérivent pas l'un de l'autre —
|
||
|
||
- **`funding` dit l'ORIGINE** (« ces watts sont achetés »), une intention, voisine du motif ;
|
||
- **`counts` dit le REGISTRE** (« ils sont comptés ici »), une comptabilité.
|
||
|
||
`counts = {allocatedW: 900, evReservedW: 480}` ne dit pas si l'achat était délibéré ;
|
||
`funding: "grid"` ne dit pas où les 900 W sont comptés. **Garder les deux est justifié — à
|
||
condition que ce soit dit**, sinon quelqu'un « simplifiera » un jour en supprimant celui qui a
|
||
l'air redondant.
|
||
|
||
### Vos trois règles sont retenues, et deux corrigent la nôtre
|
||
|
||
**`grid` dès qu'un watt est acheté** — écrit comme **règle**, pas comme approximation
|
||
(`spec_loadmodel.md` **LM-1013**). Votre argument emporte la décision : tant qu'on la décrit
|
||
comme approximative, quelqu'un voudra la rendre exacte, et sur 900/480 le libellé basculerait
|
||
d'un cycle à l'autre sans qu'aucune décision n'ait changé.
|
||
|
||
**Pas de `"mixed"` publié** — et c'est vous qui l'avez prouvé, par votre propre code : tout
|
||
client qui teste `funding == "grid"` a une branche par défaut, et cette branche est « surplus ».
|
||
Une troisième valeur y tomberait **sans erreur** et rangerait au soleil un niveau dont une part
|
||
est achetée. La panne de l'omission, avec un champ présent en plus pour faire croire le
|
||
contraire.
|
||
|
||
**Et votre ligne est au contrat** (**LM-1012**) : *on publie ce qui ne se dérive pas, on dérive
|
||
ce qui se dérive.* Votre `mixed` est légitime **parce qu'**il est dérivable et local ; le nôtre
|
||
serait illégitime **pour la même raison**. Elle explique après coup trois refus qu'on avait pris
|
||
séparément — l'index d'ordre de service, votre `mixed`, et **`stagesW`**, qui est publié
|
||
précisément parce que la combinatoire des paliers **n'est pas** dérivable chez vous. Symétrique :
|
||
`rankOrigin` est publié parce que rien dans un rang ne dit qui l'a posé.
|
||
|
||
### Vos trois demandes : deux confirmées, une qui découvre un blocage
|
||
|
||
**1. Clés opaques, jamais réutilisées pour un sens nouveau — engagement pris.** Une clé
|
||
inconnue de votre version laisse `Σ counts == targetW` vérifiable et s'affiche « destination
|
||
inconnue », jamais zéro. Nous nous interdisons de recycler un identifiant : une clé retirée ne
|
||
revient pas, comme un numéro de règle retiré (`tools/rules-retired.txt`).
|
||
|
||
**2. `counts` publié même à zéro, avec sa destination — confirmé**, et votre mesure de 06:14 est
|
||
la bonne raison : un niveau à `targetW: 0` est bien publié, et `counts: {}` satisferait
|
||
l'identité en perdant la destination. Sur le cas le plus fréquent.
|
||
|
||
**3. Qui fait foi : votre lecture est la bonne, et nous l'écrivons.** `counts` fait foi pour la
|
||
**comptabilité** ; `decision.params` fait foi pour la **phrase du motif**. Ils ne disent pas la
|
||
même chose : `params.budgetW`/`gridW` expliquent *pourquoi* la charge démarre malgré un budget
|
||
insuffisant, `counts` dit *où* les watts sont comptés. Si les deux divergeaient un jour, c'est
|
||
`counts` qui décrit le bilan.
|
||
|
||
### ⚠️ Mais R3 ne peut pas être écrite telle quelle : une de ses trois destinations n'en est pas une
|
||
|
||
En câblant `counts`, nous avons dû regarder ce que chaque compteur reçoit vraiment. **`budget.evReservedW`
|
||
n'est pas un registre d'allocation.** C'est un **terme de correction du budget**, et il mélange
|
||
deux natures :
|
||
|
||
1. la **correction A** — Σ des puissances commandées **non encore mesurées** des bornes servies
|
||
au réseau, calculée *avant* la cascade (`rulebasedscheduler.cpp:90-106`) ;
|
||
2. la **part réseau d'`EV_GRID_START`**, qui est une vraie tranche d'allocation
|
||
(`rulebasedscheduler.cpp:355`).
|
||
|
||
**Conséquence directe pour vous** : une borne servie au réseau qui tire déjà ce qu'on lui a
|
||
commandé contribue `max(0, targetW − mesuré)` = **0** à `evReservedW`, alors que son `targetW`
|
||
vaut 7 000 W. `Σ counts == targetW` serait donc **faux** — et faux de la pire façon : **vrai au
|
||
premier cycle** après un changement de consigne, faux ensuite. Ça passerait nos tests et
|
||
dériverait chez vos clients.
|
||
|
||
**Nous ne livrerons pas `counts` sur cette base.** Ce que R3 a réellement mis au jour, c'est que
|
||
**les watts achetés n'ont aucun registre aujourd'hui** : `evReservedW` corrige le budget de
|
||
surplus, il ne comptabilise pas un achat. Leur compteur, c'est `draw.committedW` — celui que
|
||
votre maquette place au §10 et qui n'existe pas encore.
|
||
|
||
**Donc `draw.committedW` devient un préalable à R3, pas une suite.** Il est petit : la somme des
|
||
`targetW` financés au réseau, publiée à côté de `budget`. Il ne change **aucune arithmétique** —
|
||
nous avons vérifié que la ligne 355 est de la **publication seule**, sans retour dans le calcul du
|
||
budget. Ce qui change est le **sens** d'`evReservedW`, qui redevient ce qu'il est : une
|
||
correction, pas un compteur.
|
||
|
||
**Votre décision, parce que c'est votre identité de réconciliation** — nous proposons :
|
||
|
||
| Destination | Ce qu'elle compte |
|
||
|---|---|
|
||
| `budget.allocatedW` | les watts **de surplus** alloués par la cascade |
|
||
| `draw.committedW` | les watts **achetés au réseau**, quelle qu'en soit la raison |
|
||
| `budget.evReservedW` | **cesse d'être une destination** — redevient une correction du budget, et le contrat le dira |
|
||
|
||
Avec cette forme, `Σ counts == targetW` tient **sans exception**, `EV_GRID_START` comprise
|
||
(900 → `allocatedW`, 480 → `draw.committedW`).
|
||
|
||
### ✅ LIVRÉ — `draw.committedW`, seul, `1.15.2+etm31`
|
||
|
||
```jsonc
|
||
"budget": { … },
|
||
"draw": { "committedW": 480 } // même trame que budget
|
||
```
|
||
|
||
**Vos trois demandes sont tenues.**
|
||
|
||
**1. Par cycle, en watts, même trame que `budget`.** Publié dans le même instantané, jamais
|
||
séparément — sinon l'identité redeviendrait temporelle par un autre chemin.
|
||
|
||
**2. Seul.** `authorisedW`, `remainingW`, `binding`, `perPhaseBound` : **absents**, pas à zéro.
|
||
Votre argument est dans le code et dans le contrat, en toutes lettres — un `authorisedW: 0`
|
||
assorti d'un `binding` par défaut afficherait un plafond qui n'existe pas, avec sa source.
|
||
|
||
**3. La définition d'`evReservedW`**, et elle justifie de changer votre libellé :
|
||
|
||
> **`evReservedW` est une CORRECTION du budget de surplus, pas une réservation.** Somme des
|
||
> puissances **commandées mais pas encore visibles au compteur** pour les charges servies au
|
||
> réseau. Elle **tend vers 0** à mesure que la mesure rattrape la commande, **sans qu'aucune
|
||
> décision n'ait changé** : c'est un rattrapage de **latence**, pas un montant mis de côté.
|
||
|
||
« Réservé au véhicule » affirme donc deux choses fausses : que c'est réservé, et que ça concerne
|
||
le véhicule plutôt que la latence de sa mesure. Quelque chose comme « commandé, pas encore
|
||
mesuré » dirait ce que c'est — mais c'est votre écran.
|
||
|
||
> **Et il n'a plus qu'une nature depuis `+etm31`.** Il recevait aussi la part réseau
|
||
> d'`EV_GRID_START`, une vraie tranche d'allocation ; nous l'avons déplacée dans
|
||
> `draw.committedW`. Sans ce déménagement, aucune définition du champ n'aurait été vraie pour
|
||
> ses deux termes — et vous auriez renommé un libellé sur une définition à moitié fausse.
|
||
|
||
`EV_GRID_START` **cesse d'être une exception** : ses deux moitiés tombent désormais dans des
|
||
compteurs de **même nature** (900 → `budget.allocatedW`, 480 → `draw.committedW`). Le partage
|
||
demeure — c'est un fait — mais il ne s'énonce plus comme une entorse. **Votre sonde peut aller le
|
||
voir sur `.75` dès que nous aurons posé `+etm31`.**
|
||
|
||
**`counts` suit maintenant**, dans la forme arbitrée : clés opaques jamais recyclées, publié même
|
||
à zéro avec sa destination, et faisant foi pour la comptabilité — les `params` gardant la phrase
|
||
du motif.
|
||
|
||
> Et deux fils antérieurs de votre brief sont **clos**, au cas où ils ne vous seraient pas
|
||
> parvenus : l'**entrelacement des domaines** est tranché — grouper à l'affichage, **jamais à
|
||
> l'écriture** (LM-1209-d) : votre note ne devrait pas être un avertissement, une liste
|
||
> entrelacée n'ayant rien d'incohérent. Et l'**écho de `rankOrigin`** est vérifié sur marque
|
||
> fraîche : l'écho verbatim passe, y compris avec un renommage ailleurs.
|
||
> **`sansRankOriginAuto()` peut partir.**
|
||
|
||
---
|
||
|
||
## 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.
|