Distinction remontée par l'agent app, qui a dû l'ajouter parce que le contrat ne
la portait pas — et confirmée sur le banc le jour même.
`none` est PUBLIÉ, donc il affirme « cette charge n'est pas mesurable ».
L'absence n'affirme rien : elle dit que la charge n'a pas participé à l'arbitrage
du cycle — mode manuel, aucun véhicule branché — ce qui est sans rapport avec sa
mesurabilité. Sans la distinction, une borne débranchée se verrait annoncer « le
compteur dédié est la seule voie » alors qu'elle publiera la sienne au prochain
cycle.
Même famille que LM-1009 D (un champ absent se voit, un champ qui s'efface se lit
comme un zéro), appliquée un cran plus haut : ici c'est l'ENTRÉE entière qui
s'efface, et la lire comme un `none` transformerait un silence en affirmation.
La garantie qui rend la règle utilisable est posée explicitement : `measurement`
est présent pour toute charge de loads[], et pour elle seule.
Constaté tel quel sur .75 en +etm26 : deux bornes pluggedIn:false absentes de
loads[], dont la V2C qui porte currentPower.
AGENTS.md consigne aussi l'état du déploiement et les trois vérifications de banc
masquées par la configuration de la box — chauffe-eau et pac-terrain portant un
meterThingId réel, elles publient `meter` et le chemin `device` n'est pas atteint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
Signalé en relayant +etm26 : « masqué dès que la charge mesure, sauf SG-Ready »
formule la règle PAR MÉCANISME. La conclusion est bonne, la clé est fausse, et le
contrat ne l'interdisait pas explicitement.
L'exception SG-Ready est déjà DANS LA DONNÉE : un sg-ready publie `none`, donc
« masquer quand source == device » ne le masque pas, sans qu'aucun client ait à
connaître le mécanisme.
Et brancher sur `adapter` se trompe sur un cas qui n'a rien de théorique : un
relay-router à topologie MIXTE publie lui aussi `none` (LM-1105-a). Un client qui
déciderait au mécanisme masquerait le champ sur un ECS qui ne peut être mesuré
QUE par un compteur dédié. Un mécanisme est un mauvais indice de mesurabilité ;
la source EST la mesurabilité.
C'est LM-1106 pris par l'autre bout : le régime est publié POUR qu'il ne soit pas
déduit. Le redéduire du mécanisme côté client remettrait dans l'app la déduction
qu'on vient de retirer du moteur — et il faudrait la maintenir à chaque mécanisme
ajouté.
Aucun code, aucune sémantique déplacée : le contrat dit désormais ce qu'il
laissait deviner. Pas de release.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
La mesure par charge lue sur l'appareil piloté, et measuredW remplacé par
measurement {source, o:powerW}. Contrat déplacé — l'app a un écran à reprendre.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
LM-1105/LM-1106 implantés. measuredW disparaît, sans alias.
ILoadAdapter::deviceMeasurement() — PURE VIRTUELLE, comme runtimeView() et pour
la même raison : un mécanisme ajouté qui hériterait d'un « non mesurable » par
défaut sortirait de la mesure sans bruit, et son écran afficherait « pas
mesurable » avec l'autorité d'un constat. Le compilateur pose la question.
Le type porte une CAPACITÉ, pas une valeur nulle : `available` répond à « cette
charge est-elle mesurable », question posée à la ThingClass, jamais à la valeur
du moment. Un routeur au palier 0 mesure 0 W et reste mesurable ; sans ce
booléen les deux seraient indistinguables.
attachMeasurements() résout meter → device → none. Le compteur explicite garde la
priorité, et pas d'abord parce qu'il mesure mieux : un réglage sans effet est
pire que pas de réglage. La priorité porte sur la SOURCE, pas sur la valeur —
compteur déclaré injoignable, on retombe sur l'appareil, parce que se taire
perdrait une mesure qu'on a et que le silence se lirait « non mesurable ».
Sous `none`, powerW est OMIS. La puissance nominale reste lisible dans
mechanism.*, qui dit d'où elle vient ; la faire passer pour une mesure produirait
un écart commandé↔mesuré identiquement nul à tous les cycles — un « tout
concorde » permanent, corollaire LM-104.
DEUX AFFINEMENTS IMPOSÉS PAR L'IMPLANTATION, tous deux de modèle :
LM-1105-a — une mesure PARTIELLE n'est pas une mesure. Là où plusieurs Things
portent la charge, il faut qu'ils la portent TOUS : trois relais dont un seul
mesure annonceraient 500 W là où 1 500 W coulent, et l'écart enverrait chercher
une panne inexistante. À ne pas confondre avec LoadTelemetry::currentPowerW, qui
accepte « au moins un » et retombe sur le nominal — celui-là est le juge runtime
du budget et doit rendre un nombre à chaque cycle. Les fusionner rouvrirait la
correction B.
LM-1105-b — la règle présuppose que le Thing piloté est TRAVERSÉ par la
puissance. Un relais de relay-router commute la charge, donc sa puissance EST
celle de la charge ; un contact SG-Ready est un contact de SIGNAL — la PAC est
alimentée ailleurs et un contact qui mesurerait quelque chose mesurerait sa
propre bobine. sg-ready est donc `none` par CONSTRUCTION, même sur des relais qui
mesurent — c'est le cas du banc — et c'est le seul mécanisme pour lequel
meterThingId reste la seule voie.
RelayRouter : la capacité se juge sur la ThingClass de TOUS les relais déclarés,
la valeur se lit sur les relais FERMÉS. Jugée sur les relais actifs, la source
vaudrait `device` au palier 1 et `none` au palier 0 — un bloc de mesure qui
apparaît et disparaît au rythme du thermostat. VÉRIFIÉ ÉCHOUANT ainsi.
testMeasurementSourceIsPublished : les trois sources sans une ligne de config de
compteur, le palier 0 qui ne fait pas basculer la source, la PAC en `none` alors
que ses contacts mesurent, la disparition de measuredW, puis les deux règles de
priorité — le compteur explicite l'emporte (1 234 contre 2 900), un compteur
injoignable fait retomber sur l'appareil.
Ne touche PAS ce que l'arbitre lit : LM-1101 tient, LoadTelemetry::currentPowerW
est inchangé, et le recrédit anti-clignotement n'a jamais transité par
meterThingId.
amd64 0 erreur / 0 avertissement. Suites, un processus par test : simulation
30/30, 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
Constat d'usage de l'agent app, retenu : pour afficher « mesuré » à côté de
« alloué », il fallait désigner une borne comme SON PROPRE compteur dans
meterThingId. Ce n'est devinable par personne, et une note d'écran qui explique
un réglage dit que le défaut est dans le modèle. LM-1105 : quand le Thing piloté
porte lui-même l'état de puissance, le moteur le lit sans configuration.
La règle porte sur le THING, pas sur le mécanisme — certains powerswitch portent
currentPower et RelayRouter sait déjà les sommer ; écrire la règle par mécanisme
y ajouterait la table à maintenir à la main que LM-600 refuse. Et le test se pose
sur la CLASSE, sur TOUS les Things déclarés : interroger les relais actifs ferait
osciller la réponse au palier 0, sur une installation dont rien n'a changé.
Le compteur explicite reste prioritaire — répondu oui, et la raison principale
n'est pas la précision mais qu'un réglage sans effet est pire que pas de réglage.
La priorité porte sur la SOURCE, pas sur la valeur : compteur déclaré injoignable
→ on retombe sur l'appareil, et la charge utile dit lequel a servi.
Le second point de l'agent app décide de la forme, et la vérification l'a rendu
plus tranchant qu'il n'était posé : le moteur porte DÉJÀ trois régimes sous un
seul nom et jette la distinction. evcharger rend une mesure ; relay-router rend
une mesure OU le palier nominal — le booléen `metered` est calculé puis jeté ;
sg-ready rend toujours une puissance DÉCLARÉE, jamais mesurée, délibérément.
Publier ces quatre lignes sous un même measuredW serait faux pour deux d'entre
elles, et un measuredW auto-rempli détruirait au passage la seule information que
le champ portait : sa présence signifiait « un compteur a été posé exprès ».
D'où LM-1106 : le régime se PUBLIE, il ne se déduit pas d'une valeur — C2 de
LM-1009 mot pour mot. measurement {source: meter|device|none, o:powerW}, powerW
absent sous none. `none` est un état POSITIF — C1 de LM-1009 : une capacité
manquante se dit — et c'est lui qui distingue « pas mesurable » de « compteur en
panne ». Sous none, la puissance nominale reste lisible dans mechanism.* : la
faire passer pour une mesure produirait un écart identiquement nul à tous les
cycles, un « tout concorde » permanent, corollaire LM-104.
Ce que la source change au diagnostic, et qui la rend utile plutôt que
décorative : device dit « la charge n'obéit pas » (commande et mesure sortent de
la même boîte) ; meter ouvre trois hypothèses — charge, compteur, câblage — dont
il ne tranche aucune ; none n'autorise rien. L'asymétrie va plus loin : sous
device une CONCORDANCE ne prouve rien, plusieurs wallbox renvoyant leur consigne
en guise de puissance, alors que sous meter elle est une preuve. device détecte
la désobéissance, meter est le seul à pouvoir détecter un mensonge — et c'est ce
qui donne à LM-1104 sa raison d'être : meterThingId reste AUTORISÉ sur une charge
qui publie déjà, à masquer par défaut, pas à interdire.
Ne touche PAS ce que l'arbitre lit : LM-1101 tient, et le recrédit
anti-clignotement continue de lire LoadContext::telemetry.currentPowerW, qui
vient de l'adaptateur et n'a jamais transité par meterThingId. Ce lot déplace ce
qui est PUBLIÉ, pas ce qui décide.
Aucun code. Le contrat bouge (measuredW → measurement), et le brief le dit à
l'app avec son coût : un écran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
La Trydan non-Pro est en WiFi ESP32 sans repli filaire, et le lien s'est révélé
inexploitable à deux mètres du point d'accès : 70 % de scrutations avortées, 12 bascules en
injoignable en vingt minutes. Le modèle sort du catalogue ETM ; seule la Pro, qui a un port
Ethernet, y reste.
Ce que la journée laisse acquis l'est : sessionEnergy existe et est CONSTRUIT par le plugin
(1.15.2+etm13 — incréments accumulés, remises à zéro détectées, redémarrage à l'ouverture
de session), currentL1/L2/L3 est déployé, phaseCount vaut 3.
Ce qui n'est pas acquis ne l'est pas : la remise à zéro de chargeEnergy observée pendant
qu'un sessionEnergy continue. C'est la seule observation qui distingue la grandeur
construite de l'alias, et elle n'a pas pu être prise. La ligne du §5 passe de « OUI, et
c'est LE point » à « REPORTÉ — Trydan Pro », et l'app n'affiche aucun avancement de charge
jusque-là.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Relevé Integrations.GetThings sur .75, borne appairée et sous tension.
· §2.1 — la classe publie currentL1/L2/L3 (5,86 / 5,94 / 6,02 A), plus powerL1/L2/L3 :
le renommage et l'unité Ampère sont déployés
· §6.4 — phaseCount vaut 3 sur la Trydan, le réglage est posé
· §2.2 — sessionEnergy EST exposée : 4,229 kWh
Mais la levée du §2.2 n'est pas acquise, et c'est le point à ne pas lire trop vite :
sessionEnergy vaut EXACTEMENT chargeEnergy (4,229419231414795 des deux côtés), ce qui est
précisément ce qu'on attend d'une session sans interruption. L'égalité ne distingue pas une
grandeur construite d'un alias du registre 0x0BC4, dont le retrait par evcc reste la raison
du refus initial.
Le test qui tranche est nommé dans le document : une remise à zéro de chargeEnergy — elle
survient à l'ARRÊT de la charge, pas au débranchement (mesuré le 28 au matin : 0,3456 kWh
→ 0, 119 s avant que le câble ne bouge) — avec un sessionEnergy qui, lui, continue. Tant
qu'elle n'est pas observée, C1 de LM-1009 reste entière et l'app n'affiche aucun
avancement. C'est elle qu'il faut guetter dans le relevé de la campagne, pas la présence du
champ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
LM-1208-b et sa conséquence LM-1208-c, trouvées au banc du 2026-08-28 en
exerçant +etm24.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
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
Quatre constats de l'agent app, vérifiés un par un sur le banc.
A — la CONCLUSION est retenue, le MÉCANISME ne l'est pas.
« Pas de voiture assignée » cesse d'être un critère d'exclusion (LM-1208-b).
Les 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. S'y ajoutent le cas de l'invité — la borne cesserait d'être arbitrée
au moment précis où elle consomme — et l'invérifiabilité d'une assignation
déclarée à la main.
Mais le budget n'était pas faussé, et c'est le journal du banc qui le montre :
pendant que la V2C tirait, le compteur est passé de −2 915 W (export) à
+3 531 W (import) et l'arbitre a publié « Surplus insuffisant (−3 531 W) —
chauffe-eau à 0 W ». L'ECS n'a pas été servi par-dessus. Le budget part de la
mesure BRUTE (invariant 8) : la consommation d'une charge exclue y est déjà
déduite. L'exclusion est conservatrice.
La règle générale est néanmoins écrite, parce qu'elle a un porteur étroit et
réel : une charge exclue du contexte mais commandée par le PROXY ne reçoit
aucun evReservedW. Transitoire, sans effet aujourd'hui — mais elle mordra au
§10, où la passe éco décide d'acheter.
Et un écart de MESURE, distinct et non tranché, est signalé : le compteur
racine lit ~1,5 kW de moins que la somme des charges mesurées. Même famille que
son aveuglement documenté aux résistances ECS. Ce n'est pas un problème
d'arbitrage, mais un budget juste suppose un compteur qui voit tout.
B — même cause, mais le mode manuel n'est pas en cause : il reste une exclusion
délibérée. Le vrai point est que le mode par défaut d'une borne détectée EST
ChargingModeNormal — une borne neuve n'est donc jamais arbitrée tant que
personne n'a choisi un mode éco. Correct, mais invisible à la mise en service.
C — l'entrée résiduelle se supprime déjà depuis l'app : SetLoadConfig remplace
l'ensemble, et l'auto-provisionnement ne recrée que les bornes dont le Thing
existe. Le geste demandé existe ; la limite à connaître est qu'une entrée dont
le Thing vit encore sera recréée — pour celle-là, c'est enabled: false.
D — instruction reçue, aucun cas particulier V2C dans le moteur, C1 maintenue.
Leur distinction est plus fine que ce que le brief anticipait : un champ absent
se voit, un champ qui s'efface se lit comme un zéro. Troisième régime, et c'est
pourquoi C2 exige que le régime soit publié plutôt que déduit d'une valeur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Question de l'agent app, relayant Patrick : « si je veux que l'énergie aille
d'abord dans l'eau chaude, et seulement ensuite dans la batterie, je fais
comment ? » Réponse : aujourd'hui il ne peut pas, et la réponse est OUI.
Les quatre constats de l'app sont exacts, revérifiés indépendamment. Et le
contrat attendait déjà cette charge : OPTIMIZER_PROTOCOL décrit
adapter:"battery" dans loads[], LoadAction porte le kind Constraint avec
Permission{Allow,Forbid} et Source{Solar,GridSource}, et SurplusBattery déclare
reserveSocPercent / maxChargeW / maxDischargeW — trois champs qu'aucun code ne
lit. domain:"battery" accepté sans effet n'est donc pas une question effleurée,
c'est le reliquat visible d'une place réservée. La note dit sous quelle forme 3f
la remplit.
Les quatre points, tranchés :
(a) Le double commandeur ne se devine pas : il se MESURE. Le plugin SunSpec
écrit, relit et compare, et son commentaire nomme le cas exact — « another
controller overwriting the setpoints ». Le résultat sort dans controlHealthy,
déjà déclaré par la classe du banc. Trois exigences en découlent, dont
celle-ci : controlHealthy == false doit SORTIR de l'arbitrage comme
available == false le fait déjà, sinon c'est le trou de pluggedIn à nouveau.
(b) batteryLevelConsideration et le rang ne sont pas concurrents par nature —
ils le sont par accident d'implémentation. Le seuil agit aujourd'hui comme
un rang absolu ; quand la batterie sera rangée, il perd ce rôle et ne garde
que celui de plancher. Le mouvement a commencé en 3g-1 avec le défaut passé
de 0,9 à 0,2, précisément parce que 0,9 confondait cible et réserve.
(c) La sûreté : « ne pas charger » et « décharger » sont deux gestes que le mot
unique cachait. Le premier ne vide rien et c'est la demande de Patrick ; le
second est déjà refusé par le budget, quel que soit le niveau. Un rang ne
fait jamais descendre un SOC, il décide de ce qui le fait monter.
(d) Le signe : l'inquiétude tombe. Charger une batterie, c'est consommer — au
compteur c'est indistinguable d'un chauffe-eau, donc allocatedW se
réconcilie sans traitement particulier. Le sens opposé n'apparaît qu'à la
décharge, et une décharge n'est pas une allocation : c'est une source, elle
appartient au revenu du budget. Règle : allocatedW d'une batterie est
toujours ≥ 0.
La règle Rules.* écartée par l'app l'est pour la bonne raison, et la note le
confirme : ce serait un second commandeur sans arbitre, pire que celui de 3g-2
puisqu'il vivrait hors du moteur, donc hors du journal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Le blocage §2.1 est fermé et vérifié sur machine : powersync-plugin-v2c
1.15.2+etm12 construit en arm64, posé par deploy.sh, ThingClass trydan chargée
avec phaseCount (Uint, min 1, max 3, défaut 1) en état ET en réglage.
Deux choses apprises en le faisant :
- Le .deb du canal powersync-testing ne convenait pas : il n'existe qu'en
amd64, et l'index arm64 du canal ne porte que abbb2x et eastron. D'où la
construction croisée.
- Le validateur de plugin refuse une classe evcharger dont phaseCount n'a pas
les bornes de l'interface. S'y conformer, c'est être lu par le moteur À
TRAVERS l'interface plutôt que par une convention propre au plugin.
Le défaut du réglage est 1 — celui du repli du moteur — pour qu'une
installation non renseignée se comporte comme avant. Conséquence : poser
phaseCount = 3 à l'appairage devient le seul geste dont l'oubli reste
silencieux. Il est en tête de la liste de contrôle, avec le moyen de le
vérifier (le minPowerW publié, pas la mémoire).
Le moteur n'a pas bougé au redémarrage : chauffe-eau 3 500 W, pac-terrain
3 000 W, degradedMode false.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
1. La réserve batterie n'annule QUE le surplus, pas une autorisation de
soutirage. Inscrit en LM-1203-b : la passe éco du §10 ne dépense pas de
surplus, elle dépense une autorisation d'acheter. Deux raisons dont chacune
suffit — une obligation qui cède à la réserve cesse d'être une obligation, et
la réserve garde son sens là où elle en a un, empêcher de vider la batterie
dans un chauffe-eau, ce qu'un soutirage réseau ne fait pas.
2. Le repli dégradé : les deux cas sont distingués, et la distinction RETIRE le
point du lot. Le plafond du §10 est AUTO-IMPOSÉ, donc un repli qui ne dépense
pas le respecte par construction — rien à écrire. L'exigence dure — tenir une
borne imposée par un TIERS alors que le moteur ne décide plus — appartient au
§14a et vivra hors du moteur. Les confondre aurait fait hériter au §10 d'une
exigence qui ne le concerne pas encore. LM-1006-1 porte désormais la table
des trois sources et de ce que « survivre » veut dire pour chacune.
3. phaseCount de la V2C : saisie à l'appairage. La Trydan du banc est
triphasée ; le repli « câbler en monophasé » est écarté, il ferait tester une
installation qui n'est pas celle du client.
Et l'attendu « sessionEnergy sur matériel réel » est RETIRÉ pour demain — le
refus V2C est documenté, pas un oubli. Remplacé par deux choses : ré-examiner
ChargeEnergy borne et référence en main, et exercer la condition C1 de LM-1009
sur ce cas réel plutôt que sur un cas fabriqué.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Deux livrables, aucun code.
DESIGN_10.md — le §10 sous la contrainte des deux notes.
Le délestage part AVEC pour une raison arithmétique, pas de calendrier : la
passe éco n'est pas bornée par le surplus mais par une autorisation de
soutirage, et cet objet n'existe pas encore. Le lot doit donc le créer.
LM-1006-1 devient une structure : DrawCap {source, totalW, perPhaseA, until} et
DrawBudget qui publie `binding` — QUI a mordu. Le piège technique est nommé et
tranché : les trois sources ne parlent pas la même langue (le disjoncteur est
par phase, le §14a et l'éco sont des totaux), un plafond total ne protège pas
d'une surcharge monophasée, un plafond par phase n'exprime pas le §14a. La
proposition est de porter les deux et de ne JAMAIS convertir — c'est la
conversion qui perdrait l'information.
LM-1003 impose la vraie refonte : les passes calculent des cibles, une seule
commande sort par charge et par cycle, arrondie une fois au mécanisme. Cela
scinde buildSetpointAction(), qui mélange aujourd'hui décision, écrêtage,
comptabilité et motif. Deux comptabilités distinctes, PlanBudget (d'où vient
l'énergie) et PlanDraw (combien on a décidé d'acheter) — les fondre rendrait
faux l'invariant publié de la première.
Cinq points à trancher, dont trois ne sont pas techniques. Le premier est le
plus lourd : la réserve batterie annule le budget de SURPLUS, mais la passe éco
dépense une autorisation de SOUTIRAGE. Si la réserve l'annule aussi, un plancher
éco ne sera jamais atteint batterie basse ; si elle ne l'annule pas, la maison
achète pendant qu'elle manque. Recommandation donnée, décision d'exploitation.
L'ordre de travail met en tête les deux étapes qui NE CHANGENT RIEN — publier le
plafond sans effet, puis la refonte décider/émettre à comportement constant,
prouvée par l'invariance des 16 scénarios enregistrés. Si un seul point de
contrôle bouge, c'est qu'une décision a changé, et il faut savoir laquelle.
PREPA_BANC_20260828.md — deux blocages silencieux trouvés la veille.
1. La ThingClass trydan ne déclare PAS phaseCount, et aucun registre Modbus V2C
ne le porte. EvCharger::phaseCount() retombe sur 1 : sur une Trydan
triphasée, le plancher publié vaudrait 1 380 W au lieu de 4 140, et la
conversion W→A donnerait une consigne 3× trop haute. C'est exactement la
famille de défaut que 3g-2 vient de fermer, rouverte côté plugin. Correctif
proposé : saisie à l'appairage, comme la voiture simulée le fait déjà.
2. sessionEnergy n'est délibérément pas exposée — le brief du plugin l'interdit,
evcc ayant retiré son ChargeRater pour cette borne (firmware peu fiable).
Donc « sessionEnergy sur matériel réel » n'est PAS franchissable demain. En
échange, demain permet la validation qui lèverait l'interdit, et exerce pour
de vrai la condition C1 de LM-1009 sur du matériel.
Plus : le plugin V2C n'est pas installé sur le banc, la Skoda n'a aucune
intégration (donc voiture manuelle, avec les quatre réglages à saisir), et
l'IP de test .127 est libre.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Décisions de Patrick, écrites dans la spec et portées à l'agent app qui avait
remonté la question.
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 et se lit comme une lecture du véhicule. Le pourcentage
reste dans la saisie de l'intention, il sort de l'affichage de l'avancement.
D2 — obligation par SESSION d'abord. Elle ne demande que de lire un champ déjà
publié, c'est l'usage réel (on branche le soir pour partir le matin), et surtout
la remise à zéro de sessionEnergy au branchement CESSE d'être une réserve : pour
une obligation de session, c'est la bonne sémantique. Le texte est réécrit en
conséquence — le problème disparaît au lieu d'être contourné. L'obligation
quotidienne devient un lot à part, dépendant de l'historique du §11.
D3 — tant que le chemin carBatteryLevel survit, il est étiqueté à la
publication. Une valeur que le moteur a lui-même intégrée ne franchit pas la
frontière RPC sous le même nom qu'une mesure. Précédent de nommage déjà dans ce
document (estimatedPowerW), et même refus que le currentPowerStorage à zéro du
relevé 3g-1 : mieux vaut « estimé », ou rien, qu'un chiffre crédible et faux.
Les deux réserves deviennent des CONDITIONS DE LIVRAISON, pas des notes — ce
sont les deux façons dont le §10 peut mentir sans erreur visible :
C1 — une borne sans sessionEnergy dégrade VISIBLEMENT. Mesuré : wallboxNoMeter
et l'une des deux classes wallbox ne l'ont pas. Une échéance qui cesse
silencieusement d'être mesurable est pire qu'une échéance absente, parce
qu'elle continue de s'afficher. Et « pas mesurable » n'est pas « rien livré ».
C2 — on doit pouvoir dire LEQUEL des deux régimes s'applique, dans le motif
comme à l'écran. Un avancement mesuré et un avancement estimé ne se
distinguent par aucune de leurs valeurs : les deux sont un nombre qui monte.
La distinction des deux régimes est conservée telle quelle : une obligation en
énergie peut annoncer son atteinte, une cible en pourcentage publie une
intention et un avancement estimé — jamais « tenue ».
Le brief app gagne un cinquième point : c'est celui qui change un écran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Question remontée par l'agent app. Note de conception, aucun code : elle
conditionne la forme du §10, elle ne la code pas.
Le constat de l'app est exact — vérifié sur les cinq classes evcharger du banc,
aucune ne déclare batteryLevel ni ne porte d'interface véhicule. Mais le code
dit quelque chose de plus troublant que « il manque une mesure » :
targetPercentage est comparé à carBatteryLevel, lu sur le Thing voiture. Et ce
batteryLevel, dans le seul cas où le mécanisme s'active — une voiture manuelle,
dont l'état est INSCRIPTIBLE — est écrit par le moteur lui-même :
updateManualSoCsWithMeter() 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. Le moteur compare donc sa cible à sa propre estimation,
bâtie sur trois grandeurs dont aucune n'est mesurée. La boucle se referme sur
elle-même. Et l'estimation sort par la frontière RPC indistinguable d'une
mesure — c'est l'app qui ment ensuite, de bonne foi.
Pendant ce temps, la seule grandeur réellement mesurée n'est lue par personne :
sessionEnergy est publiée par quatre des cinq classes, LoadContextTelemetry
porte déjà son emplacement (sessionWh), et le champ n'est rempli nulle part.
Réponse à la première question : OUI, et les trois pièces existent déjà —
sessionEnergy pour mesurer, sessionWh pour la transporter, needs.minEnergyWhPerDay
pour exprimer la cible. C'est la forme que le §10 doit retenir : une obligation
en ÉNERGIE, vérifiable de bout en bout sans communication véhicule. Avec quatre
réserves écrites, dont deux mordent tout de suite : sessionEnergy se remet à
zéro au branchement (donc inutilisable telle quelle pour un minEnergyWhPerDay),
et toutes les bornes ne la publient pas (donc la dégradation doit être VISIBLE).
Réponse à la seconde : OUI, et elle tient MÊME avec la première — parce que
targetPercentage ne disparaît pas. Deux objets, deux régimes de vérité : une
obligation en énergie peut annoncer son atteinte, une cible en pourcentage
publie une intention et un avancement inconnu ou estimé, jamais « tenue ». Même
exigence que BELOW_MIN_POWER contre SURPLUS_INSUFFICIENT : deux causes, deux
phrases, parce qu'elles n'appellent pas le même geste.
Deux arbitrages sont laissés ouverts, dont un de lot : obligation par session
(faisable en lisant un champ déjà publié) ou obligation quotidienne (qui exige
l'historique du §11).
LM-1205 (§12) renvoie désormais vers cette note : la machinerie d'échéance
existe, son avancement ne se mesure pas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Quatre points, émis vers l'agent app.
Le deuxième est le plus utile pour lui : la mise en garde « pour une borne,
allocatedW est la décision de l'arbitre et non l'état de la borne » TOMBE.
L'arbitre commande seul depuis +etm23 — 9 commandes sur la fenêtre du banc, 9
par lui, 0 par le proxy. allocationIsCommand rend true partout, les branches
conditionnelles peuvent disparaître.
Avec une réserve qu'il faut lui laisser : l'allocation reste ce qui a été
COMMANDÉ, pas ce que la borne TIRE. Ce qu'il peut enfin affirmer, c'est que
personne d'autre ne commande. L'écart commande/réalité se lit dans la charge
utile `mechanism`, pas dans allocatedW.
Les trois autres : loads[] ⊆ GetLoadConfig rétabli (2 → 4 entrées sur le banc,
avec la forme d'une entrée evcharger et la neutralité de l'aller-retour) ;
EV_GRID_START {budgetW, floorW, gridW} avec sa comptabilité partagée — clé à
prévoir, mais PAS ENCORE VUE sur machine, donc pas d'écran à bâtir autour ;
et le rang de borne, qui se classe au glisser-déposer sur une seule liste.
La réserve sur le rang par défaut est signalée explicitement parce qu'elle le
concerne : avant tout réglage, l'ordre affiché est un artefact, pas un choix.
À ne pas présenter comme un réglage tant que le point n'est pas tranché.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
executeChargingAction() écrit cette ligne à CHAQUE commande envoyée à une
borne — c'est la ligne qu'on lit en premier quand on cherche ce qui a été
commandé, et elle est citée telle quelle dans les relevés de banc.
Aucun test ni aucun outil ne filtre sur ce mot : vérifié par grep sur tout
l'arbre. Les journaux cités dans docs/RELEVE_3g2.md gardent la graphie
d'origine — un relevé transcrit ce que la machine a dit, il ne le corrige pas
après coup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
+etm24 posé sur `.75`, condition du §3.1 rejouée à l'identique : wallbox en
Eco, voiture assignée, pluggedIn false. La borne sort du contexte, le journal
le dit, et le budget va entier aux deux charges configurées. Sous +etm23 la
même condition lui donnait 4 853 W.
Une conséquence est notée parce qu'elle n'est pas évidente : « hors arbitrage »
ne veut pas dire « non commandée ». Sortie du waterfall, la borne retombe sur
le proxy, qui la commande à son courant minimum. L'invariant du lot tient — un
seul commandeur — et aucun watt n'est retiré de la cascade. Mais les trois
exclusions ne finissent pas au même endroit, et ça se saura le jour où le §10
fera du proxy autre chose qu'un repli.
Banc rendu à son état d'origine, voiture de test retirée, wallbox éteinte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Trouvé au banc le 2026-08-27, en exerçant 3g-2 (docs/RELEVE_3g2.md §3.1).
Une wallbox en Eco, voiture assignée, mais `pluggedIn: false`, s'est vu allouer
4 853 W qu'elle ne pouvait pas tirer — `currentPower: 0`, `charging: false`. La
PAC derrière elle n'a survécu que sur son propre recrédit. Trois cycles
consécutifs : 2 350 / 4 853 / 5 464 W retirés du budget et donnés à personne.
evParticipatesInWaterfall() reprenait le filtre d'entrée du proxy — voiture
assignée, mode non manuel — mais pas `pluggedIn`. Le proxy n'en avait pas
besoin : son allocation n'était pas une cascade, donc gaspiller ne privait
personne. En cascade, si. L'asymétrie était déjà visible dans le code : la
boucle EV du scheduler, qui sert les financements réseau, teste
`!ev->pluggedIn()` depuis toujours ; seul le chemin waterfall ne le faisait pas.
C'est le défaut du relevé 3g-1 §3.4 par une autre porte : là le plancher
mentait, ici c'est le filtre d'entrée. Défaut de 3g-1, révélé par 3g-2 parce
que la décision atteint enfin le matériel — et corrigé à part du lot, comme les
quatre précédents.
Hors arbitrage, et non « servie à 0 avec un motif » : même famille que les deux
autres exclusions (DESIGN_3g §4). Une borne débranchée ne consomme rien, sa
consommation nulle est déjà au compteur, et lui donner un rang la ferait
apparaître dans la cascade pour n'y rien faire. Le journal l'annonce comme les
deux autres cas.
testUnpluggedChargerTakesNoBudget, VÉRIFIÉ ÉCHOUANT SANS LE CORRECTIF : la
borne débranchée disparaît du contexte et la charge suivante reçoit les 3 000 W
entiers ; la seconde moitié rebranche le véhicule pour prouver que c'est bien
`pluggedIn` qui décide, et non une exclusion fortuite.
Suite complète : simulation 28/28, charging 16/16, loadmodel 16/16,
spotmarket 7/7. Build amd64 0/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
`1.15.2+etm23` construit arm64, posé sur `.75` par deploy.sh, exercé.
FRANCHI, journal et RPC à l'appui :
- Une charge, un commandeur. 9 lignes « Executing action Simulated wallbox »
sur la fenêtre, 9 AVANT « Adjusting chargers » (dispatch de l'arbitre), 0
après (proxy). 6 cycles sur 6 portent « is commanded by the ETM waterfall
this cycle. Not re-deciding here. » Le doublet du relevé 3g-1 §3.3 a
disparu, et quelqu'un commande — l'arbitre a bien allumé la borne à 10 A
sur sa propre décision.
- Le plancher publié est l'exigence réelle : 2 070 W sous +etm22, 4 140 W
sous +etm23, MÊME borne, MÊME journée. Et il suit le nombre de phases visé
d'un cycle à l'autre (4 140 en triphasé, 1 380 en monophasé).
- L'entrée evcharger est créée d'office, journalisée une fois par borne,
sans charge utile ; l'aller-retour GetLoadConfig → SetLoadConfig verbatim
est neutre et ne reconstruit aucun adaptateur ; loads[] ⊆ GetLoadConfig
est rétabli.
TROUVÉ, et c'est le point du passage machine :
- GRAVE — une borne SANS VÉHICULE BRANCHÉ absorbe le budget de la cascade.
4 853 W alloués à une wallbox `pluggedIn: false` qui tire 0 W, la PAC
derrière ne survivant que sur son propre recrédit. Trois cycles
consécutifs, 2 350 / 4 853 / 5 464 W donnés à personne. C'est le défaut du
relevé 3g-1 §3.4 par une autre porte : là le plancher mentait, ici c'est le
filtre d'entrée. evParticipatesInWaterfall() reprend le filtre du proxy —
voiture assignée, mode non manuel — mais pas pluggedIn. Le proxy n'en avait
pas besoin : son allocation n'était pas une cascade. Défaut de 3g-1,
correction PROPOSÉE ET NON APPLIQUÉE, à traiter dans son propre commit.
- Mineur — le rang par défaut des bornes auto-provisionnées retombe à
ÉGALITÉ avec le rang 1 déjà occupé, au lieu de « en tête ». Le tri reste
total et reproductible, mais ce n'est pas ce qui était demandé.
NON OBSERVÉ, et pas fabriqué : EV_GRID_START (le budget est passé de 385 W à
2 350 W sans s'arrêter dans la fenêtre de tolérance), BATTERY_RESERVE avec un
surplus réel, LM-1204-b matériel (la Terra AC ne répond pas à l'ARP depuis le
banc lui-même), et le cas de transition — aucune borne n'était sous commande au
moment de la bascule.
Banc rendu à son état d'origine : ChargingInfos identiques champ par champ,
voiture de test retirée, wallbox éteinte. Les entrées evcharger restent — elles
seraient recréées au prochain démarrage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Entrée de changelog du lot 3g-2, des quatre correctifs de 3g-1 qui
l'accompagnent et des deux [upstream-fix] du même passage.
Rien n'est construit ni déployé : le banc .75 tourne encore +etm22. La
construction arm64 et le déploiement restent à faire, et docs/RECAP_3g2.md §7
donne l'ordre de vérification au banc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Note de conception. Aucun code, aucun champ, aucune entrée au modèle de
charges : elle existe pour que la porte reste ouverte.
Le §14a EnWG permettra au gestionnaire de réseau d'imposer un plafond de
puissance au point de livraison, que l'EMS répartit ensuite librement sur les
appareils. C'est le même mécanisme que le délestage exigé par LM-1006, avec
une source de commande différente. Il y a donc trois sources pour un seul
mécanisme : le disjoncteur de branchement (physique, permanent), le §14a
(externe, sur ordre, temporaire), et le plancher éco du §10 (implicite, celui
qu'on s'impose en décidant de soutirer).
Ce que la note demande, et c'est tout : quand le délestage général sera écrit,
ne pas l'écrire comme « la protection de surcharge » mais comme « appliquer un
plafond de soutirage ». La source est un paramètre, pas une hypothèse enfouie
dans le nom de la fonction. Cela tient dans une signature aujourd'hui ; ce
serait une refonte dans deux ans.
Deux propriétés sont vraies des trois sources et ne coûtent rien à tenir dès
l'écriture :
- Le plafond le plus contraignant l'emporte, et on doit pouvoir dire LEQUEL
a mordu. C'est l'exigence qui sépare déjà BELOW_MIN_POWER de
SURPLUS_INSUFFICIENT : un client bridé doit lire « gestionnaire de réseau »
et non « protection de surcharge ». Deux causes, deux gestes — l'une se
subit, l'autre s'attaque en délestant ou en revoyant l'abonnement.
- Le plafond survit à la panne du HEMS. Vrai du disjoncteur par
construction ; ce sera une obligation légale pour le §14a, où la coupure
totale est interdite mais le plafond doit tenir. C'est une contrainte sur
degradedMode, et ce n'est pas celle qu'on aurait devinée : le repli L2
d'aujourd'hui est conçu pour NE RIEN INITIER, pas pour continuer à faire
respecter une borne qu'un tiers a imposée. Autant le concevoir là-dessus
maintenant que le découvrir en certification.
Referme au passage une référence pendante : LM-1006-1 était cité par §13 sans
avoir jamais été défini.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Le relevé machine du 2026-08-26 l'énonçait sans détour : « tant que ceci n'est
pas traité, la transplantation ne commande rien ». L'arbitre décidait,
publiait un motif juste, et adjustEvChargers() commandait autre chose une
milliseconde plus tard. Ce lot ferme les trois points que le banc a ouverts.
1. UNE CHARGE, UN COMMANDEUR
Les actions EV du waterfall sont enfin DISPATCHÉES — elles ne l'étaient pas du
tout : m_loadAdapters ne contient pas les bornes, et applyActionsToAdapters()
s'arrêtait là. Et les bornes ainsi commandées sortent du chemin proxy par deux
crochets virtuels posés dans l'amont, interrogés à deux moments du cycle où la
réponse n'est pas la même : evSurplusPlannedByWaterfall() avant le plan (à qui
la borne appartient-elle), evCommandedByWaterfall() après (qu'a-t-il commandé).
Sans cette distinction, une borne servie par l'échéance ne serait plus
commandée du tout.
Le jeu des bornes commandées est NOMMÉ par le scheduler (Plan::waterfallEvIds).
Le redéduire ailleurs — « motif différent d'EV_DEADLINE », « financement au
surplus » — dériverait du vrai au premier motif ajouté, et EV_GRID_START porte
justement funding=Grid tout en venant du waterfall. Il est vidé à l'entrée du
mode dégradé : le waterfall ne commandant plus, il ne peut plus revendiquer.
chargingState, que posait adjustEvChargers(), est repris par l'arbitre pour
les bornes qui n'y passent plus — sinon l'app afficherait « en charge » une
borne que l'arbitre vient d'éteindre.
2. evFloorW() REND L'EXIGENCE RÉELLE, ET LA TOLÉRANCE DEVIENT VISIBLE
Le plancher valait `min × acquisitionTolerance × 230 × phases` : 2 070 W
annoncés pour une borne qui en exige 4 140. Le waterfall allouait 2 828 W,
l'adaptateur refusait d'enclencher sous 6 A, et 2 828 W étaient retirés du
budget pour personne. Ce sont deux grandeurs différentes : le plancher est ce
que la borne EXIGE, la tolérance est la part qu'on accepte de ne pas couvrir.
La tolérance n'est pas supprimée pour autant — un réglage documenté (défaut
0,5) qui devient inerte sans le dire est un silence ambigu, et tout le parc
verrait son VE démarrer deux fois plus tard sans explication. Elle devient un
seuil de démarrage EXPLICITE : motif EV_GRID_START {budgetW, floorW, gridW},
funding=Grid, et une comptabilité qui se partage — budgetW dans
budget.allocatedW, gridW dans budget.evReservedW, budgetW + gridW ==
allocatedW.
C'est une brèche assumée dans le financement réseau que DESIGN_3g §3.1 déférait
à 3f. Elle est bornée par construction, pas par discipline : surplus réel exigé
(budgetW > 0), tout le budget restant consommé donc AU PLUS UNE borne par
cycle, et le plafond de la protection de surcharge s'applique avant. Écrite en
toutes lettres dans DESIGN_3g §3.1-bis.
3. LE RANG D'UNE BORNE EST CONFIGURABLE
Il vit là où vivent tous les rangs : LoadConfig.priority (LM-1206/LM-1207). Pas
de second système de priorité — 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.
Toute borne détectée reçoit d'office une entrée « evcharger », portant le rang
et RIEN d'autre : ses limites viennent du Thing et changent avec la voiture
branchée. Création journalisée, une ligne par borne, à la création seulement —
une écriture que personne n'a demandée doit laisser une trace. Le tri devient
TOTAL : à rang égal, l'identifiant départage, donc laquelle démarre est
reproductible même avant tout réglage.
L'entrée SURVIT à la disparition de son Thing : borne remplacée, ThingId
changé, appareil déposé. Elle reste publiée available: false / faultCode:
THING_MISSING / motif LOAD_UNAVAILABLE, et ne retient aucun budget. La
supprimer d'office rendrait « borne remplacée » indistinguable de « borne
jamais configurée ».
CORRIGE UNE RUPTURE D'INVARIANT DE 3g-1, non signalée à l'époque : en faisant
entrer les bornes dans loads[], 3g-1 publiait des charges qui ne figuraient
dans AUCUNE GetLoadConfig — la charge fantôme que le lot B-bis avait
supprimée, réapparue par l'autre bout. L'app s'appuie sur cet invariant pour
résoudre libellé, domaine et rang. L'auto-provisionnement le rétablit par
construction.
ÉPROUVÉ
- testEvChargerRankDecidesWhoCharges : deux bornes, un budget qui ne paie
qu'un plancher, la seconde refusée avec BELOW_MIN_POWER {budgetW: 620,
minPowerW: 1380}, et l'inversion du rang change la borne servie. C'est la
moitié ARBITRAGE de LM-1204-b. Couvre aussi l'aller-retour neutre par la
frontière RPC et l'inclusion loads[] ⊆ GetLoadConfig.
- testEvGridStartIsAnnouncedAndBounded : le motif chiffre le soutirage, la
comptabilité se réconcilie, une seule borne soutire par cycle.
- testEvChargerConfigOutlivesItsThing : l'entrée survit, publiée en défaut.
- testEvChargerConfigIsRankOnly / RefusesPayloads / ClaimsItsThing.
Deux points de contrôle de `run` bougent, dans un seul scénario et par une
cause unique : la conversion watts→ampères se fait désormais après
l'allocation, et l'arrondi tombe parfois de l'autre côté. 14 des 16 scénarios
sont identiques au bit près, et ce qui compte est inchangé dans les deux —
la voiture est à 100 % à l'échéance.
Suite complète : simulation 27/27, charging 16/16, loadmodel 16/16,
spotmarket 7/7. Build amd64 0/0.
RIEN N'A TOURNÉ SUR MACHINE. docs/RECAP_3g2.md dit ce qui reste à éprouver au
banc — dont LM-1204-b avec deux bornes réelles, la Terra AC étant vérifiée
absente du réseau (ARP INCOMPLETE depuis le banc lui-même).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
LM-1203-b dit « le budget est ANNULÉ, pas réduit », et le motif publié dit
« aucune charge n'est servie ». Le code disait autre chose : la réserve ne
mordait que sur les AUGMENTATIONS.
Le recrédit anti-clignotement (correction B) rend à chaque charge sa
consommation de début de cycle avant d'arrondir. Il le faisait aussi sous la
réserve — donc toute charge déjà en marche gardait exactement sa puissance,
indéfiniment, pendant que la télémétrie annonçait qu'aucune n'était servie.
Le recrédit suppose que le budget est une MESURE dont la consommation de la
charge a déjà été retranchée. Sous la réserve, le budget n'est pas une mesure
mais une DÉCISION de retenir : recréditer dedans rend exactement ce qu'on
venait de confisquer. Le recrédit est donc supprimé dans ce seul cas, pour les
charges en watts comme pour les états SG-Ready — sans quoi une PAC en état 3
ou 4 y resterait elle aussi indéfiniment.
Conséquence sur le motif : ce que la réserve retient à une charge a DEUX
termes, et il faut les deux. Le surplus annulé, le même pour toutes, et sa
propre consommation, que le recrédit lui aurait rendue. Sans le second, une
charge en marche coupée par la réserve sur un surplus nul publiait « surplus
insuffisant » — vrai sur le surplus, faux sur la cause : sans la réserve, elle
aurait continué de tourner.
Le défaut ne s'est vu qu'en exécutant réellement les décisions du waterfall :
batteryLevelConsiderationStopCharging échouait, la borne restant allumée là
où l'amont la coupe.
Suite charging vert (dont les deux tests de réserve), simulation `run` 16/16,
testEcsSurplusPV / testSgReadySurplus / testLoadTelemetryRpc verts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Quatrième défaut latent de 3g-1. Il vaut son propre message parce que c'est
une décision, et qu'elle s'est prise contre le test.
3g-1 convertit l'enveloppe du waterfall en ampères puis la passe à
`static_cast<uint>(qBound(...))` — donc une TRONCATURE. Cela paraît prudent
(« ne jamais dépasser l'enveloppe ») et coûte systématiquement un ampère :
230 W par phase, abandonnés à chaque cycle, toute la journée. Les scénarios de
simulation le chiffrent — huit d'entre eux sortaient un ampère plus bas, et
la voiture finissait la journée 1 % moins chargée.
L'arbitrage : l'enveloppe est une ALLOCATION, pas une limite. Elle sort d'une
mesure de compteur dont le bruit dépasse largement 115 W. Ce qui est une
limite — le courant maximal de la borne et la place laissée par la protection
de surcharge — n'est pas franchi pour autant : le plafond est déclaré à
l'enveloppe (commit précédent), donc respecté AVANT l'allocation, et le qBound
écrête au matériel.
C'est pourquoi le CODE a été réaligné sur l'amont plutôt que le test :
testOverloadProtectionEcoMode encode le comportement de l'arrondi, et la
troncature faisait basculer la protection de surcharge de « réduire au
minimum » à « couper ». Un ampère de conversion ne doit pas décider ça.
Et c'est aussi pourquoi la limite de phase n'est PAS réappliquée en ampères
entiers dans l'adaptateur : ce second écrêtage serait plus strict que celui de
l'amont (plancher au lieu d'arrondi) et coûterait de nouveau un ampère, pour
69 W de marge théorique que la couche L4 mesure réellement en temps réel.
Suite simulation `run` 16/16, testOverloadProtectionEcoMode vert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Troisième défaut latent de 3g-1, et le seul des trois qui touche la sécurité.
Corrigé à part du lot 3g-2, pour pouvoir être relu ou rétroporté seul.
L'écrêtage d'une consigne EV par la limite de phase vivait dans
planSurplusCharging() : `newValue = qMin(newRawValue, allowanceOnRootMeter)`.
En transplantant la décision de surplus dans le waterfall, 3g-1 a laissé cet
écrêtage derrière lui. Sans conséquence tant que rien n'était exécuté — mais
la décision du waterfall est censée l'être, et alors plus rien ne borne
l'allocation avant qu'elle ne parte au matériel. Le seul filet restant serait
verifyOverloadProtection(), APRÈS coup.
Le plafond redevient donc une borne DÉCLARÉE de l'adaptateur
(LoadContext.declared.maxPowerW), ce qui le place là où la règle 4 d'AGENTS le
veut : le waterfall ne peut pas allouer au-delà, au lieu d'être rattrapé.
Deux points de sécurité tenus explicitement :
- Le plafond ne descend JAMAIS sous le plancher de la borne. C'est la règle
de l'amont reprise mot pour mot (`if (newValueInt <= minValue) newValueInt
= minValue`) et la répartition des rôles qu'elle porte : la limite de
phase RABAISSE la borne à son minimum, elle ne la coupe pas. Couper est le
geste de la couche L4, qui tourne en temps réel sur powerBalanceChanged et
garde le dernier mot après le dispatch. Écrêter jusqu'à 0 ici ferait
décider la coupure par la stratégie.
- L'allowance est la place LIBRE, sans rendre à la borne ce qu'elle tire
déjà. Le retour serait plus juste en régime établi, mais ferait disparaître
la réponse à la surcharge : sur une phase en dépassement, la place libre
est NÉGATIVE, et c'est ce signe qui ramène la borne à son minimum.
Nouveau motif PHASE_LIMIT {limitW, requiredW} : quand la place libre tombe
sous ce que la borne exige, c'est la limite de phase qui interdit, pas le
budget. Dire « budget sous le plancher » enverrait attendre le soleil là où
il faut regarder l'abonnement et les autres consommateurs — deux causes, deux
gestes (règle 7-b).
Suite simulation `run` 16/16, testOverloadProtectionEcoMode vert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Deuxième défaut latent de 3g-1, corrigé à part du lot 3g-2 pour la même
raison que le précédent.
EvAdapter passait `currentPower / 230` comme courant de référence à la règle
de bascule 1↔3 phases, au motif — écrit dans le commentaire — que
l'allocation n'est pas encore connue à cet instant. Mais la bascule ne se
décide pas sur ce que la borne TIRE : elle se décide sur ce qu'elle POURRAIT
tirer. C'est la formule du proxy, `currentPower/230 + allowance`, et c'est la
seule qui puisse répondre « redescends en monophasé, il n'y a plus que
800 W ».
Avec le seul courant courant, une borne passée en triphasé y restait quel que
soit l'effondrement du surplus — et son plancher publié restait à 4,1 kW,
donc plus rien ne pouvait la servir. Le surplus, lui, EST connu avant
l'allocation : c'est la mesure du compteur.
Le terme de stockage de planSurplusCharging() (tampon de 500 W, réinjection
au-dessus du seuil) n'entre volontairement pas dans cette référence : il la
déplace à la marge et n'a d'effet que sur le choix 1↔3, jamais sur
l'allocation — laquelle est faite par le waterfall, avec un budget qui tient
compte du stockage.
Suite simulation `run` 16/16, charging vert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Défaut de 3g-1, resté latent parce que sa sortie n'atteignait que la
télémétrie. Il est corrigé à part du lot 3g-2 pour pouvoir être relu,
contesté ou rétroporté sans lui.
EvAdapter interrogeait EvCharger::canSetPhaseCount(), qui dit seulement que
la borne PORTE l'état desiredPhaseCount. La vraie question — la borne ET la
voiture acceptent-elles 1 et 3 phases — est tranchée une fois par cycle par
prepareInformation(), qui en tire canSwitchPhaseCount et le couple
min/maxPossiblePhaseCount. Les deux prédicats ont divergé : là où le proxy
appliquait `qMin(carPhaseCount, getBestPhaseCount(...))`, 3g-1 n'a repris que
l'appel.
Conséquence : une borne dont la combinaison ne permet qu'une phase se voyait
viser 3 phases. Son plancher publié triplait (1,4 kW → 4,1 kW) sans que rien
ne puisse jamais le franchir — la borne restait éternellement « sous son
plancher », avec un motif juste et une cause fausse.
Second défaut, dans applyAction() : le nombre de phases demandé était borné
par m_charger->phaseCount(), qui est l'état COURANT de la borne. Une borne en
monophasé ne pouvait donc jamais être commandée en triphasé, et le plancher
publié pour trois phases n'aurait de toute façon pas pu être servi.
evTargetPhases() cesse d'être un passe-plat et lit les bornes du manager, via
un accesseur [ETM] read-only de plus (internalProcessInfo). L'arbitre ne
redérive rien : une seconde version de cette règle divergerait de la première
au premier ajustement, ce qui est exactement ce qui vient d'arriver.
Suite charging 16/16, simulation `run` 16/16, testLoadTelemetryRpc vert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Deux écarts entre le contrat écrit et le code, dans le même type.
Le premier accompagne le correctif de la description publiée : INTERFACE.md
répétait la promesse fausse à trois endroits, dont une note d'intégration qui
la formulait en toutes lettres (« seuls les champs présents sont appliqués »).
La table des défauts obtenus est désormais donnée, ainsi que le geste correct.
Le second n'avait pas de correctif de code parce que le code a raison :
`repeatDays` était documenté « 0=Lun … 6=Dim » alors que setChargingInfo()
refuse tout ce qui sort de [1, 7] — un client qui suit la documentation reçoit
EnergyErrorInvalidParameter sur le lundi. Les bornes de `targetPercentage`
sont documentées au passage, pour la même raison.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
La description publiée par l'introspection JSON-RPC annonçait « Only given
properties will be set, others will be untouched. » C'est faux : unpack()
construit un ChargingInfo NEUF depuis la charge utile, et setChargingInfo()
remplace le stocké en entier. Un champ writable omis retombe donc sur son
DÉFAUT — targetPercentage à 80, repeatDays vide, endDateTime absent,
assignedCarId nul — et l'appel réussit, EnergyErrorNoError. Rien ne signale
la perte.
Constaté sur le banc .75 le 2026-08-26 en restaurant la configuration : un
appel ne portant que chargingMode a porté targetPercentage de 0 à 80.
C'est la DESCRIPTION qui est corrigée, pas le comportement, et le choix
mérite d'être dit : ce .so est un drop-in de l'amont. Une sémantique de
fusion casserait le seul geste qui permet d'EFFACER un champ — retirer une
échéance, vider repeatDays — et changerait un contrat que les clients
existants suivent déjà. La description énumère désormais chaque défaut et
prescrit le geste correct : lire avec GetChargingInfos, modifier, renvoyer
l'objet complet.
Le test épingle le comportement annoncé, en trois temps : un objet complet,
un appel partiel qui aboutit et perd les champs omis, puis l'aller-retour
qui les préserve — y compris quand la charge utile relue porte
chargingState, propriété en lecture seule que la méthode doit ignorer sans
faillir. Il porte sur le COMPORTEMENT décrit, jamais sur la non-vacuité
d'une réponse.
Vérifié au passage : aucun autre Set* du namespace ne porte cette promesse
ni ce comportement. SetPhasePowerLimit, SetAcquisitionTolerance,
SetBatteryLevelConsideration et SetLockOnUnplug sont scalaires ;
SetSpotMarketConfiguration ne remet jamais un champ omis à son défaut ;
SetLoadConfig annonce déjà un remplacement d'ensemble.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
prepareInformation() repliait sur ChargingModeEco toute borne dont
nextEndTime() est invalide, dès lors que son mode n'était pas
ChargingModeNormal. Or ce repli n'a de sens que pour les modes qui EXIGENT
une échéance — c'est bien ce que font les deux autres sites du même repli
(planSpotMarketCharging, planSurplusCharging), qui testent explicitement
EcoWithTargetTime / EcoMinWithTargetTime. C'est ce site-ci qui a perdu la
garde en 7588473, quand les deux modes ETM ont été ajoutés.
Deux conséquences, toutes deux silencieuses :
- ChargingModeEcoWithMinCurrent était réécrit en ChargingModeEco au cycle
suivant, PERSISTÉ et notifié par chargingInfoChanged. Le mode « éco +
courant minimum garanti » ne survivait donc jamais à un cycle, et la
branche d'adjustEvChargers() qui l'applique était morte. Du point de vue
de l'app, un réglage posé par l'utilisateur disparaissait tout seul.
- ChargingModeEco était « replié » sur lui-même à CHAQUE cycle :
setChargingMode, storeChargingInfo (écriture disque) et une notification
chargingInfoChanged, sans qu'aucune valeur ne change. Un *Changed qui ne
change rien, à chaque tour, pour chaque borne en éco.
Trouvé en écrivant le test d'aller-retour de SetChargingInfo, qui échouait
sur la réécriture du mode.
Suite charging 16/16, simulation `run` 16/16.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
Le lot E §12 a ajouté le paramètre minPowerW au constructeur
d'EtmVariableLoadAdapter sans le répercuter sur les cinq sites d'appel de
tests/auto/simulation. La suite ne compile donc plus depuis ce commit —
autrement dit, personne ne l'a exécutée depuis. Les cinq sites passent
désormais minPowerW = 0 explicitement.
Deux attentes avaient rouillé pendant ce temps, et les deux portent sur du
TEXTE publié — ce qui est précisément ce que ces tests doivent surveiller :
- testEcsColdStartLockExpires attendait « insuffisant » là où f134048 a
délibérément fait sortir BELOW_MIN_POWER. Le cas est « il y a un budget,
il n'est pas au bon FORMAT » : le motif doit nommer le palier. Ce que le
test éprouve reste le même — ce n'est pas le verrou.
- testDecisionReasonRendering attendait « Surplus insuffisant » pour
SG_NORMAL, reformulé en « Aucun surplus à allouer » par le même commit,
parce que ce motif ne sort plus que pour un budget nul ou négatif.
Le catalogue de motifs avait dérivé au passage : BATTERY_RESERVE et
BELOW_MIN_POWER étaient entrés en 3g-1 sans ligne de rendu attendue ni entrée
dans la liste de clôture. Leur texte — celui sur lequel le banc s'appuie pour
distinguer « il n'y a rien » de « il y en a, mais pas au bon format » ou
« une règle le retient » — n'était donc éprouvé par rien. Refermé, avec les
deux formes de BELOW_MIN_POWER (avec et sans état).
Suite verte : simulation 24/24 fonctions, dont les 16 scénarios de `run`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
La transplantation ne commande rien tant que adjustEvChargers() n'est pas traité : l'arbitre
refuse, publie un motif juste, et le proxy commande ON à 6 A une ligne plus loin. La même
ligne part vers la Terra AC réelle et n'a rien fait UNIQUEMENT parce qu'elle est hors ligne.
Le plancher EV vaut la moitié du seuil de démarrage réel — acquisitionTolerance, qui est un
seuil de déclenchement, a été pris pour un plancher. 2828 W alloués, 4,1 A demandés, 6 A
exigés : allocation perdue.
La bascule 0,4 / 0,9 est faite dans les deux sens et la correction withheldW: 0 est vérifiée
sur machine. La moitié qui manque — la réserve retenant un surplus RÉEL — demande du soleil.
LM-1204-b n'est pas franchi : il exige deux bornes arbitrées, la Terra AC ne répond pas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
buildTelemetry() n'interrogeait que la table des charges de configuration et sautait l'EV
avec un commentaire devenu faux (« décidé en amont du waterfall »). Le journal montrait les
décisions des deux bornes ; GetLoadTelemetry n'en montrait aucune. Le changement de contrat
annoncé par +etm21 n'était donc pas livré — et LM-1204-b était inéprouvable, puisque son
discriminant est justement le motif publié.
Nouveau champ loads[].funding (« surplus » / « grid ») : dès lors que les bornes sont dans
loads[], deux d'entre elles peuvent y être pour des raisons différentes, l'une comptée dans
budget.allocatedW, l'autre dans budget.evReservedW. Sans lui, sommer allocatedW donne un
écart ininterprétable.
Constat non corrigé, écrit au contrat : le rang d'une borne n'est pas configurable
(priority = 100 en dur) et std::sort n'est pas stable — laquelle des deux bornes est servie
n'est ni réglable ni garanti reproductible.
Aussi : « batterie à 50 %% » — %% n'est pas une échappée pour QString::arg. Vu au banc.
Et retrait du code mort buildMinCurrentAction/buildIdleAction, seuls porteurs de EV_ECO_MIN
et EV_IDLE depuis +etm21.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Session arrêtée sur contexte saturé. Le paquet 1.15.2+etm21 est CONSTRUIT mais le banc
tourne encore etm20 : la transplantation n'a jamais tourné une seule fois, sur un banc
qui porte une Terra AC réelle. C'est la première chose à faire, et LM-1204-b avec.
Le RECAP porte la commande exacte de reprise, les cinq points à vérifier au
redéploiement, et ce qui reste ouvert — dont la bascule 0,4 / 0,9 devenue possible
depuis que SunSpec déclare energystorage, et jamais faite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
La transplantation. Les bornes partagent le budget, décrémentent la cascade et portent
un rang qui a enfin un sens — il était décoratif depuis le début.
Le défaut corrigé n'était pas théorique : planSurplusCharging() calculait l'allocation
une fois avant la boucle et ne la décrémentait jamais. Chaque borne recevait la totalité
du surplus. Avec 4 kW et deux bornes, ce n'était pas « 2 kW à chacune » mais « 4 kW à
chacune », et le rattrapage venait du compteur au cycle suivant, pas de l'arbitrage.
Trois motifs EV disparaissent — des suppressions, pas des remplacements. EV_ECO_MIN en
particulier : le courant minimum était la même idée que le plancher du §12, écrite une
seconde fois pour un seul mécanisme.
Le nombre de phases est décidé AVANT l'allocation et réutilisé à l'exécution, parce que
c'est lui qui fixe le plancher — 6 A valent 1,4 kW en mono et 4,1 kW en tri. C'était le
piège le plus silencieux du lot.
evReservedW est restreint au financement réseau plutôt que supprimé : ce qui reste
décidé en amont doit rester réservé, ce qui passe par la cascade ne le doit plus.
Et une correction attrapée à la première observation en vrai de BATTERY_RESERVE : il
était publié avec withheldW à 0 sur un surplus négatif, donc il disait « une règle vous
bloque » là où il n'y avait simplement rien.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Un filterByInterface qui rend une liste vide est toujours ambigu entre « il n'y a pas de
batterie » et « il y en a une, mal déclarée ». Le second désactive une règle de sécurité
sans produire ni erreur ni panne — même mode de défaillance qu'un seuil de réserve mal
réglé : rien ne se signale.
Constaté sur le banc : le Thing « SunSpec Storage » déclare battery mais pas
energystorage. La réserve ne s'est donc JAMAIS appliquée — ni dans le waterfall que je
viens d'écrire, ni dans le proxy amont qui utilisait le même filtre depuis toujours, ni
dans le bilan de puissance de l'experience-plugin qui filtre pareil. GetPowerBalance rend
currentPowerStorage: 0 pendant que la batterie affiche 50 % et « Holding » : elle n'est
pas à zéro, elle est invisible.
L'arbitre émet maintenant un avertissement quand des appareils portent 'battery' sans
'energystorage'. Il ne corrige rien — la déclaration appartient au plugin — mais il rend
la cause lisible au lieu de la laisser deviner.
Brief émis vers etm-powersync-plugins-modbus : ce n'est pas une ligne de JSON, l'interface
exige currentPower et capacity que la classe ne fournit pas, et un currentPower de
batterie à zéro serait pire que son absence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Le vrai sujet n'était pas le banc mais le défaut d'usine. Une fois la règle batterie
dans le budget, un seuil à 0,9 gèle toute l'installation sous 90 % de SOC — ni ECS, ni
PAC, ni borne — sans erreur et sans panne. Personne ne s'en apercevait tant que la règle
ne touchait que le VE.
Le réglage confondait une CIBLE et une RÉSERVE. Une cible est ce qu'on aimerait
atteindre, une réserve ce sous quoi on ne descend pas ; la règle « sous ce niveau, rien
d'autre n'est servi » a une sémantique de réserve et était défaultée comme une cible.
0,2 protège l'autonomie du soir sans confisquer la journée, et un installateur qui veut
« la batterie d'abord » monte la valeur délibérément — maintenant que l'écran l'expose.
BATTERY_RESERVE publie le surplus RETENU, pas seulement le SOC : « 3 200 W retenus » se
comprend là où « batterie à 47 % » se subit.
LM-1203-b inscrit la nuance qui ne se déduit pas du code : sous la réserve le budget est
annulé, pas réduit.
Vérifié au passage, comme demandé : evReservedW devra filtrer sur funding == Grid après
la seconde moitié — spot market et échéance restent au proxy, minCurrent et idle
disparaissent. Il reste correct tel quel tant que le VE n'est pas dans la cascade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Première moitié de 3g-1. La règle batterie vivait dans planSurplusCharging(), donc dans
le chemin VE — par accident d'histoire, pas par conception. Ce n'a jamais été une règle
« véhicule » : c'est une règle de BUDGET. Tant qu'elle y restait, une batterie sous sa
réserve empêchait de charger la voiture mais laissait le chauffe-eau se servir : deux
poids, deux mesures pour la même énergie.
buildContext() remplit désormais ctx.battery (champ qui existait, resté vide depuis
« déféré 3f »), et le waterfall applique les quatre comportements de l'amont :
sous la réserve le budget est ANNULÉ (et non réduit — servir « un peu » retarderait la
recharge sans démarrer personne, planchers du §12) ; batterie en charge sous le seuil,
tampon de 500 W ; en charge au-dessus, sa puissance rejoint le budget ; en décharge, sa
puissance est retranchée quel que soit le niveau, car ce qu'elle fournit n'est pas du
surplus.
⚠️ CHANGEMENT DE COMPORTEMENT assumé pour l'ECS et la PAC, qui n'ont jamais connu cette
règle. Une installation qui servait ses charges sous le seuil de réserve ne le fera plus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Livrable de phase avant tout code (workflow AGENTS.md). Écrit après lecture des
1 900 lignes de smartchargingmanager.cpp.
Le constat central est plus dur que le scénario ne le laissait croire.
planSurplusCharging calcule allowanceInAmpere UNE fois, avant la boucle, et ne le
décrémente jamais : chaque borne reçoit la TOTALITÉ du surplus, comme si elle était
seule. Avec 4 kW et deux bornes, le proxy ne produit pas « 2 kW à chacune » — il
produit « 4 kW à chacune ». Ce qui rattrape n'est pas l'arbitrage mais le compteur au
cycle suivant, puis la protection de surcharge. Le système converge par correction
d'erreur, pas par décision. Le critère LM-1204-b doit donc se lire : avant 3g les deux
bornes démarrent puis sont rattrapées ; après, une seule démarre, délibérément.
Trois choses ne se transportent pas, et deux demandent une décision non technique :
1. L'ÉCHÉANCE — sa machinerie (projectedEndTime, remainingPercentageToCharge) vient du
SoC de la voiture et n'a aucun équivalent ailleurs ; et le scheduler ne lit pas
encore needs (« grid funding dormant jusqu'à 3f »). Forcer pour tenir une échéance,
c'est soutirer, donc franchir la frontière du §10. Recommandation : la laisser au
proxy, qui maigrit sans disparaître.
2. LA BATTERIE — batteryLevelConsideration est calculé dans le chemin VE alors que
c'est une règle de BUDGET. La monter change le comportement de l'ECS et de la PAC,
qui ne l'ont jamais eue. Recommandation : la monter quand même, et l'assumer.
3. LE NOMBRE DE PHASES — se décide en ampères, le waterfall parle en watts : la
conversion descend dans l'adaptateur. L'ordre s'inverse, et le piège est silencieux :
une borne qui passe en triphasé change son plancher effectif (1,4 → 4,1 kW), donc le
plancher publié doit correspondre au nombre de phases VISÉ, pas au courant.
Nuance sur le plancher : minimalChargingCurrent vaut max(borne, voiture), il dépend
donc de la voiture branchée. C'est une valeur runtime, pas un réglage — l'écran ne doit
pas la présenter comme tel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Audit demandé par Patrick : un motif qui décrit la cause générique là où le mécanisme
connaît la cause précise. Deux branches en portaient le défaut, et une troisième le
porte ailleurs.
Branche à paliers : un budget de 300 W devant un premier palier de 500 W publiait
SURPLUS_INSUFFICIENT. Ce n'est pas un manque de surplus, c'est une granularité.
Branche sg-ready : le code publié était SG_NORMAL — correct sur le fond, donc l'app n'a
jamais été trompée. C'est le RENDU FRANÇAIS qui disait « Surplus insuffisant (933 W) »
devant un état 3 à 1 500 W, et c'est l'installateur qui lit journalctl pendant une mise
en service qui l'était. Le paramètre state nomme désormais l'état hors d'atteinte :
dire « sous le plancher » sans dire lequel remplacerait une phrase vague par une autre.
La distinction conservée est celle que Patrick a posée : budget ≤ 0 laisse
SURPLUS_INSUFFICIENT / SG_NORMAL — il n'y a rien. Budget positif sous le premier palier
donne BELOW_MIN_POWER — il y en a, mais pas au bon format.
Troisième cas, non corrigé ici : EV_IDLE dit « pas de surplus » là où la borne a un
plancher de 6 A. Son site est le proxy amont, que 3g remplace ; l'y corriger
maintenant reviendrait à écrire deux fois la même règle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Défini avec Patrick quand le banc a reçu une seconde borne. Il n'est PAS exerçable
avant 3g, et c'est le point : nonEvLoads filtre etmvariableload | relay-router |
sg-ready, l'EV est décidé en proxy AVANT le waterfall, donc les deux bornes ne
partagent aucun budget et leur rang n'a aucun sens.
Le piège que Patrick signale est réel et il a une réponse observable : une wallbox
simulée peut obéir à une consigne sous son plancher là où du matériel refuserait. Le
discriminant est le MOTIF publié, que seul le scheduler peut émettre —
BELOW_MIN_POWER prouve que l'arbitre a refusé et qu'aucune commande n'est partie,
SURPLUS_SETPOINT avec setpointW < minPowerW prouve l'inverse, et une borne à 0 W sans
motif de plancher signifie que c'est le matériel qui a refusé, donc que le test ne
prouve rien.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Le domaine d'une consigne devient {0} ∪ [minPowerW, maxPowerW], un ensemble discontinu.
LM-1203 est la règle qui fait le travail : sous le plancher, allouer 0 et TRANSMETTRE le
budget à la charge suivante.
Constat en lisant le waterfall : il satisfaisait déjà LM-1203 pour les charges à
paliers — le plus haut palier ≤ budget vaut 0 quand le budget est sous le premier, et
le résidu cascade. Le trou n'était que dans la branche continue, où
qBound(0, budget, max) acceptait n'importe quelle miette. Le correctif est donc plus
petit que la spec ne le laissait craindre, et localisé.
Le plancher est déclaré pour etmvariableload dynamic et dérivé partout ailleurs — plus
petit palier non nul chez relay-router, plus petite estimation non nulle chez sg-ready.
SetLoadConfig refuse de le déclarer là où il se dérive, et refuse aussi minPowerW >
maxPowerW : le domaine serait réduit à {0}, la charge ne démarrerait jamais, et rien
dans le journal ne dirait pourquoi.
Motif BELOW_MIN_POWER distinct de SURPLUS_INSUFFICIENT : les confondre enverrait
chercher du soleil là où il faut lire une fiche technique.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Vérifié sur .75 après déploiement de +etm17 : GetLoadConfig rend meterThingId='' et
sensorThingId='' sur les deux charges du banc, alors que le fichier persisté n'en porte
aucune trace. C'est la sérialisation par méta-objet, exactement comme domain — et c'est
ce qui rend l'aller-retour Get → Set neutre, donc ce sur quoi l'app s'appuie pour
écrire. Un client traite '' et l'absence de la même façon.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Deux champs optionnels de même forme sur LoadConfig — o:meterThingId, o:sensorThingId —
et leur publication en télémétrie. Rien d'autre : l'arbitre ne s'en sert pas pour
décider, et le §12 comme le §10 restent à faire.
« Aucun effet sur les décisions » est vrai par CONSTRUCTION, pas par discipline : les
adaptateurs ne voient jamais ces Things. attachMeasurements() les résout au moment de
publier, dans les deux chemins de télémétrie — y compris le mode dégradé, où un
exploitant veut précisément voir ce que la charge fait pendant que l'arbitre ne décide
plus rien.
measuredW est publié À CÔTÉ de allocatedW et jamais fondu dedans : l'un est ce que le
waterfall a alloué, l'autre ce qu'un compteur a vu. Les confondre rendrait la somme des
allocations irréconciliable avec budget.allocatedW — et injecter une mesure en retard
sur la commande réintroduirait l'oscillation que le recrédit a supprimée.
claimedThingIds() les exclut, et l'exclusion est maintenant commentée sur place : LM-201
vaut pour ce qui commande, ceux-ci mesurent. Sans ce commentaire, la symétrie avec
relays[] invitait à les ajouter — ce qui ferait refuser un compteur divisionnaire
partagé ou une sonde de pièce commune.
spec 0.2.1 : la précondition de livraison du §10 (extension du délestage au waterfall,
même lot ou avant, jamais après) est écrite comme CONDITION et non comme conseil — la
protection de surcharge ne déleste que les bornes, et des planchers éco qui
s'additionnent un matin sans soleil sur un branchement 6 kVA feraient disjoncter
l'installation. Et LM-1008 tranche la cible de confort : saisie en degrés, frontière de
l'arbitre en énergie, conversion à la couche domaine — et aucune conversion nécessaire
aujourd'hui, un comparateur suffit, c'est l'arrêt qui compte et pas le dosage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Trois manques du modèle, spécifiés AVANT implémentation. Rien à coder tant que
Patrick n'a pas relu.
§10 — Besoin à deux niveaux (LM-1000+). L'éco n'est pas un champ nouveau : c'est
`needs {dailyDeadline, minEnergyWhPerDay}` promu au rang de plancher. Deux passes sur
la MÊME liste ordonnée, soit un ordre lexicographique sur (niveau, rang) — dérivé du
rang existant, donc pas un second système de priorité ; la paire
prioritéEco/prioritéConfort est rejetée explicitement. Deux règles que la conception
appelle et que le brief ne demandait pas : une seule commande par charge et par cycle
(sinon deux commutations dans le même cycle, du court-cycling fabriqué par l'arbitre),
et un recrédit anti-clignotement attribué une seule fois.
La frontière est nommée : la passe 1 n'est pas bornée par le surplus mais par une
AUTORISATION DE SOUTIRAGE, objet distinct que le rule-based rendra binaire et le MILP
continu. Et deux couplages sans lesquels la conception est fausse : la limite de
raccordement — les planchers éco s'additionnent sur le disjoncteur, et la protection
de surcharge ne déleste aujourd'hui QUE les bornes — et l'ordre de service apparent,
qu'un plancher de rang 4 passant devant un confort de rang 1 rend contre-intuitif.
§11 — Compteur et sonde (LM-1100+). Sélection par interface nymea, jamais par plugin.
Le compteur vérifie, il ne décide pas : ne pas l'injecter au budget, il est en retard
sur la commande et la charge est déjà comptée au compteur racine — décomposition, pas
source. Un écart de mesure ne pose PAS available:false : ce serait retirer le service
sur un doute de mesure. Et ni le compteur ni la sonde ne sont exclusifs, contrairement
aux relais : LM-201 ne s'applique pas à ce qui mesure, donc validateSet() ne doit pas
les inclure dans son contrôle d'unicité.
§12 — Le plancher (LM-1200+). Le domaine de la consigne devient {0} ∪ [min, max], un
ensemble discontinu. Déclaré pour etmvariableload seul ; dérivé pour les deux autres —
même règle qu'au lot D pour stagesW. La règle d'allocation qui compte : sous le
plancher, allouer 0 et TRANSMETTRE le budget, sinon une charge de rang 1 bloque tout
le waterfall en gardant un budget qu'elle ne peut pas utiliser. Décision arrêtée : la
priorité porte sur la borne, pas sur la voiture ; pas de priority dans ChargingInfo,
qui ne saurait pas exprimer « VE1 > ECS > VE2 ».
§13 — Ordre recommandé, et ce n'est PAS celui de l'énoncé : §11 d'abord (additif,
risque nul, et la sonde constitue l'historique sans lequel les seuils du §10 ne se
règleront jamais — le différer coûte du temps CALENDAIRE, pas du développement), puis
§12 (contenu, débloque 3g puis l'unification du rang), puis §10 en dernier — et son
délestage général dans le même lot, pas après.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H