L'agent plugin a mesuré l'inverse sur la même box : écho verbatim, "auto" compris,
EnergyErrorNoError. L'exception existe déjà côté moteur — reconcileRankOrigin() accepte
"auto" quand le persisté le porte déjà, écrite exprès pour ne pas rompre la neutralité
Get → Set.
Ce que j'avais mesuré est autre chose : entre ma lecture et mon écriture, la marque avait
été consommée par une manipulation concurrente. Mon écho portait donc un "auto" que le
persisté ne portait plus — un écho périmé, correctement refusé. Deux cas distincts, un seul
symptôme, et c'est précisément pourquoi une écriture qui ne relit pas juste avant ne prouve
rien. Le contrôle de la suppression, lui, relit avant de conclure ; ma sonde jetable, non.
§10 corrigé : la ligne du tableau dit maintenant ce qu'elle mesure vraiment, et ce qui
reste à mesurer (un écho inchangé sur une marque FRAÎCHE). sansRankOriginAuto() est
documentée comme un contournement EN SURSIS — elle part au résultat de cette mesure, et la
marque survivra alors aux gestes qui ne la concernent pas. Rien n'est retiré d'ici là.
Ce qui reste vrai de mon relevé : "user" s'écrit, ne déplace pas le rang, survit aux gestes
voisins. Et l'effacement de "auto" que j'ai observé est le fait de l'APP — de son
nettoyage — pas d'un refus de la box.
── Deux pièges de sonde, consignés dans tools/rpc/README.md ─────────────────
1. Un refus arrive en `status: "success"`, verdict dans params.energyError. Une sonde qui ne
teste que `status` annonce « accepté » sur une écriture rejetée.
2. Les identifiants ne s'écrivent pas pareil d'un appel à l'autre — le §9, documenté la
VEILLE, et la sonde y est quand même tombée : elle rendait des verdicts sur une charge
qu'elle ne regardait pas.
La leçon n'est pas « écrire des sondes plus soigneusement » : c'est que la sonde ne partage
pas les garde-fous de l'app. Quand la question porte sur ce que l'app fera, importer son
modèle vaut mieux que relire le JSON ; quand elle porte sur la box, croiser la réponse RPC
avec journalctl avant de conclure.
Et la raison lisible d'un refus n'existe QUE dans ce journal — côté RPC il ne reste qu'un
code. Silence de refus ouvert depuis le premier jour, rencontré une fois de plus, et raison
d'être des miroirs clients comme validateSet.
Rien de touché côté entrelacement : ça attend le moteur.
226 tests inchangés, analyze inchangé à 27.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Les règles d'affichage de `measurement` sont figées par test sur des trames fabriquées.
Cette sonde les éprouve sur les trames réelles de la box — en important le modèle de l'app
(LoadTelemetryEntry.fromJson), pas en relisant le JSON à sa façon : une lecture parallèle
ne prouverait rien sur ce que l'écran fera.
Elle montre ce qu'un écran regardé de loin ne distingue pas : `none` (bloc masqué, « pas
mesurable ») d'une charge simplement absente de loads[] (rien à conclure), et la portée
d'un écart selon `meter` ou `device`.
Relevé du 2026-08-28 sur .75 en 1.15.2+etm26 :
measuredW encore publié ? false
chauffe-eau source=meter → Mesuré 1500 W · mesure indépendante : concordance = preuve
pac-terrain source=meter → Mesuré 800 W · idem
V2C Trydan absente de loads[] — pas `none`
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Le piège des accolades n'était pas un détail de chargingModeOf(). C'est la deuxième
occurrence en une semaine d'un motif qui se répétera : deux appels JSON-RPC de la même box,
portant le même objet, n'emploient pas la même convention, et RIEN ne le dit. Écrit comme
tel dans docs/UI_data_contract.md §9, avec renvoi depuis les deux sites de code et depuis
tools/rpc/README.md — là où quelqu'un croise deux réponses brutes.
Les occurrences, leur symptôme :
· GetPowerBalance (currentPowerProduction…) × GetPowerBalanceLogs (production…) →
tout tombe à zéro, l'historique paraît vide sur 7 960 échantillons bien présents
· GetChargingInfos (evChargerId sans accolades) × GetLoadConfig (id avec) →
« mode non lu » sur une borne parfaitement lue, et la note « hors arbitrage » muette
· famille voisine, même silence : currentPower* en W × total* en kWh, facteur 1000
Ce qui en fait un motif et pas trois anecdotes : le symptôme n'est JAMAIS une exception,
c'est une valeur neutre plausible, et un test unitaire qui construit ses deux côtés avec la
même convention passe aussi. D'où les trois gestes écrits au §9 — sonder les deux appels
côte à côte sur les charges utiles brutes, éprouver le rapprochement sur les deux formes
réelles, et se méfier d'un vide qui ne coûte rien à expliquer.
Et le lien avec une règle qu'on suivait déjà sans la relier à ça : une valeur neutre issue
d'un croisement raté est indiscernable d'une vraie absence. « Non lu » laisse une chance de
trouver le bug ; « 0 » l'enterre.
── Campagne V2C arrêtée, et la réserve sessionEnergy avec elle ────────────────
La Trydan non-Pro est en WiFi ESP32 sans repli filaire, lien 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 Trydan Pro (port Ethernet) y reste.
La trace qui lève C1 — chargeEnergy à zéro AVEC un sessionEnergy qui continue — n'a donc
pas été prise et ne viendra pas de cette borne. Les notes restent en place telles quelles,
avec la raison mise à jour : elles seront levées sur une Trydan Pro. Aucun avancement de
charge affiché d'ici là.
164 tests, flutter analyze inchangé à 27 remarques.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Vérification demandée après le relevé V2C du banc : « ta note de rangs ex æquo doit
s'afficher — vérifie qu'elle le fait ». Elle s'affiche. Mais rien ne le prouvait, et
c'est exactement le défaut qui a coûté isNotGrouped : la contiguïté était calculée
depuis le premier jour, testée en fonction, exposée par le provider — et affichée nulle
part. Un test de fonction ne peut pas voir ça.
Quatre tests de widgets sur la configuration réelle du 2026-08-28
(loadconfig_hems75_v2c.json), montés dans le harnais existant :
· la note de rangs ex æquo est à l'écran ET nomme les charges (chauffe-eau, V2C
Trydan) — « des rangs sont à égalité » sans dire lesquelles laisse chercher dans
cinq cartes ;
· la note d'ordre entrelacé est à l'écran ET annonce ce que l'enregistrement fera
(REGROUPERA) — l'annoncer après serait un constat, pas un avertissement ;
· contrôle négatif : sur un ordre groupé et classé, aucune des deux n'apparaît. Sans
lui, deux notes toujours affichées deviennent du décor ;
· THING_MISSING se lit « l'appareil n'existe plus dans nymea » + « réinstallez-le, ou
retirez cette charge », et n'offre RIEN à lever — ni le bouton, ni l'aide qui
l'accompagne.
⚠️ La télémétrie du quatrième test est COMPOSÉE, pas capturée : le banc publiait
WRITE_FAILED sur cette charge au dernier relevé (l'adaptateur épuise son échelle
d'écriture avant que THING_MISSING ne sorte). Le test éprouve le rendu du code sur le
loadId réel de la Terra AC supprimée, pas la présence du code sur la box.
Et deux traces de la campagne V2C, qui ne se câblent pas :
· tools/rpc/evcharger_watch.dart entre au dépôt, documenté. C'est la sonde qui a
produit les deux constats bloquants — une borne hors arbitrage n'apparaît pas dans
telemetry_watch, et c'est pendant ces transitions qu'il faut la regarder.
· ev_charging_card.dart affirmait que « les deux bornes du banc publient sessionEnergy ».
Faux depuis l'arrivée de la V2C : elle publie chargeEnergy, remis à zéro à chaque
interruption de charge. La note renvoie désormais à LoadMechanism et redit C1 de
LM-1009 — « pas mesurable », jamais zéro.
TODO : entrée d'attente pour sessionEnergy et le renommage currentL1/L2/L3.
146 tests passent, flutter analyze sans remarque nouvelle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
Le miroir INTERFACE_etmvariableload.md est passé en rév. 3 pendant la session (corps
identique au canonique du dépôt plugin, vérifié). Son §10 s'adresse nommément à l'agent
app — et il RATIFIE les maquettes dessinées aujourd'hui : relays[] = {thingId, powerW}
pour l'ECS relais, maxPowerW seul pour le continu, powerLevels absents de la config et
dérivés par le routeur.
Trois recalages :
- Le picker de relais filtre sur l'interface nymea « power » et présente les things par
NOM — nommé dans la maquette, il ne l'était pas.
- Le point 4 du brief plugin est tranché par la rév. 3 : la duplication de la
combinatoire est voulue (l'app dérive pour montrer, le routeur pour exécuter). La
demande devient donc un moyen de détecter la divergence — la télémétrie publie déjà
maxStageW, l'app peut y confronter le maximum de sa propre liste.
- En-tête du pont VM Service, que le commit précédent avait laissé dans sa forme brute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
flutter run n'imprime pas les traces dart:developer log() — elles partent au VM
Service, ni sur stdout ni dans logcat. Ce pont est ce qui a rendu le passage sur
appareil du 2026-08-25 exploitable ; il ne vivait que dans un scratchpad de session,
alors que le RECAP le donnait « à récupérer au prochain passage ».
Mode d'emploi en tête, avec le piège déjà payé une fois : le VM Service livre
l'historique bufférisé d'un coup au streamListen, donc horodater à la réception
écrase toute la chronologie sous une seule seconde. On lit event.timestamp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Trois sondes permanentes, et le relevé de ce qu'elles ont montré sur .75.
- telemetry_watch.dart (lecture seule) : cadence du battement de cœur et avancée
du cycle. C'est la seule mesure qui distingue une installation stable d'un
moteur arrêté — les deux produisent le même flux de trames.
- domain_assign.dart (écrit, --yes) : classe des charges en important le code
d'écriture de l'app. Une sonde qui sérialiserait à sa façon ne prouverait rien
sur l'app.
- fault_probe.dart (écrit, --yes) : provoque une charge en défaut, éprouve
ClearLoadFault, restaure. La config d'origine part sur le disque AVANT la
première écriture et la restauration rejoue ce fichier verbatim — filtrer la
sonde hors d'une relecture serait une reconstruction, c'est-à-dire le chemin
qui a failli écraser la config du banc via persist(). La config relue est
ensuite comparée clé par clé, pas seulement « la sonde a disparu ».
Ce que le banc a appris, et qui ne se déduisait pas du code :
- ClearLoadFault lève RÉELLEMENT le verrou (journal : « défaut LEVÉ ») et le
cycle suivant reverrouille, la cause n'ayant pas disparu. La télémétrie ne
repasse jamais à available:true. Une app qui aurait cru l'acquittement aurait
menti deux secondes plus tard.
- Le défaut publié est WRITE_FAILED, pas THING_MISSING : l'écriture est tentée,
échoue, l'échelle s'épuise, et m_faulted masque le second code.
- SetLoadConfig refuse en bloc et le motif n'est PAS dans la réponse RPC — il est
au journal. L'app ne peut que rapporter un refus, pas l'expliquer.
- ECS-412 confirmé au journal : une écriture de domaine ne reconstruit aucun
adaptateur (« 2 inchangée(s), 0 retirée(s) »). Le §7-1bis n'est plus une
lecture de code.
.75 est laissé classé (chauffe-eau→ecs, pac-terrain→hvac) : métadonnée que
l'arbitre ne lit pas, et le multi-domaines y devient exerçable. Retour arrière :
domain_assign.dart --clear --yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
Range dans tools/rpc/ les sondes qui vivaient dans un scratchpad volatil :
probe (appel RPC quelconque), notifications (abonnement + comptage),
locale et locale_diff (comportement o:locale et son effet réel),
loadconfig_roundtrip (aller-retour neutre §7-1). README avec usages.
loadconfig_roundtrip est le SEUL qui écrit sur la box : garde --yes ajouté
pour qu'un SetLoadConfig ne parte jamais par réflexe de flèche haut.
analysis_options : tools/** et les classes l10n générées sortent de
l'analyse — flutter analyze revient à 25 issues (ligne de base).
Fixtures : le lot plugin télémétrie EST déployé sur .75 depuis ce matin
(20 méthodes / 14 notifications). Capturé avant que ça ne se reperde :
- loadtelemetry_hems75.json : dump réel de GetLoadTelemetry
- loadconfig_hems75_v2.json : GetLoadConfig avec la clé 'domain'
- schema_hems75_v2.json : schémas SET + télémétrie introspectés
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>