Deux gestes que le relevé du banc a tranchés, dans les deux sens.
1. THING_MISSING — MESURÉ, la réserve tombe
GetLoadTelemetry sur .75, 2026-08-28 10:29 UTC : la Terra AC supprimée est publiée
available=false · faultCode=THING_MISSING · decision=LOAD_UNAVAILABLE{frozenW:0} ·
mechanism.kind=evcharger. Le code SORT. Le test de rendu ne repose plus sur une trame
composée mais sur cette capture (test/fixtures/loadtelemetry_hems75_thingmissing.json).
Le masquage relevé mardi (etmvariableloadadapter.cpp:148-150, m_faulted testé avant que
THING_MISSING ne puisse s'exprimer) concerne l'adaptateur des charges VARIABLES — relais
et SG-Ready. L'adaptateur evcharger n'a pas ce chemin, d'où la différence. Il n'est donc
pas corrigé, seulement contourné par le hasard du chemin de code : remonté au brief
moteur avec la trame, parce que les deux codes envoient le client faire deux gestes
différents — « écriture échouée » fait démonter un coffret, « appareil absent » fait
rouvrir nymea.
2. EVChargingCard — elle part, et la monter aurait publié de la fiction
Trois des grandeurs qu'elle affiche ne sont JAMAIS lues sur la box : chargingMode,
chargingPower (3,6 kW) et solarSourcePercent (82 %) sont des valeurs par défaut de
EnergyData — _updateEnergyData ne les touche pas, et le mode n'est écrit qu'en optimiste
après une commande, jamais relu par GetChargingInfos. Sa feuille « Paramètres de charge »
proposait en plus un courant minimum, qui n'existe dans aucun contrat, et son bouton
« Enregistrer » ne faisait que Navigator.pop : rien n'était écrit. Ce n'était pas du code
mort à ranimer, c'était un écran à réécrire depuis le modèle.
⚠️ Et la surface vivante est pire que la morte : _EVWidget (favorites_screen.dart:419)
affiche les MÊMES valeurs par défaut comme si elles étaient mesurées — « 3600 W »,
« 82 % solaire », « Mode PV » — et écrit le mode sur des pastilles de 7 à 10 px sans
restituer un refus. Entrée 🔴 au TODO, avec ce qu'une carte de recharge peut dire
aujourd'hui : le mode lu par GetChargingInfos, la puissance mesurée du Thing, l'échéance
comme intention saisie. Ni SOC (n'existe pas), ni chargeEnergy (remis à zéro à chaque
interruption), ni courant minimum, ni phaseCount.
146 tests passent. flutter analyze : 27 remarques, deux de moins qu'avant — les deux
étaient dans le fichier supprimé.
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
RELAIS : mon avertissement de reconstruction sur un réordonnancement est devenu FAUX, et
il est corrigé. `+etm16` compare la TABLE que les relais produisent (deriveStages), plus la
liste par index. Le miroir Dart suit, y compris le détail qui compte : les ThingIds d'une
combinaison se comparent comme un ENSEMBLE — collectés dans l'ordre de déclaration, fermer
{K1,K2} ou {K2,K1} est le même geste électrique. Mais l'appariement palier par palier
reste positionnel, ce qui préserve le seul cas où l'ordre signifie encore quelque chose :
deux relais de MÊME puissance, où il décide quel contact sert ce palier.
Conséquence pour l'écran : réordonner des contacteurs de puissances distinctes n'avertit
plus de rien. Avertir sur un geste devenu gratuit serait aussi trompeur que de se taire sur
un geste coûteux.
PALIERS : l'app ne dérive plus la combinatoire pour l'affichage — la box les publie
(mechanism.stagesW). Vérifié sur appareil : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500].
deriveStages reste côté app pour la seule annonce d'impact, qui se fait avant l'écriture.
Tant que le brouillon n'a pas touché aux contacteurs, c'est la table PUBLIÉE qui s'affiche ;
dès qu'il y touche elle devient périmée, et l'écran montre une prévision annoncée comme
telle plutôt qu'une table qui décrit l'état d'avant.
COMPTEUR ET SONDE : ils ne « vont pas arriver », ils SONT au schéma de .75
(o:meterThingId, o:sensorThingId). Le grisage dérivé les a donc activés seul, sans nouvelle
version de l'app — exactement ce qui était promis. Mais un bloc actif sur un placeholder
mort est pire qu'un bloc grisé : les deux sont désormais câblés sur un vrai sélecteur,
filtré par INTERFACE (smartmeter, temperaturesensor) et jamais par nom de plugin.
Ni l'un ni l'autre n'entre dans sameHardware() : les rattacher ne reconstruit rien. Ni dans
le contrôle d'unicité : deux charges peuvent partager une sonde, un compteur divisionnaire
en couvrir plusieurs — le sélecteur ne grise donc rien ici, contrairement aux contacteurs.
Et la mesure sert à VÉRIFIER, jamais à décider : elle n'entre pas dans le budget, la charge
étant déjà comptée dans le racine dont elle n'est qu'une décomposition.
COMPTEURS CUMULÉS : la base purgée, les valeurs sont saines (55 730 / 50 / 6 327 / 62 007
kWh) et la tuile « compteur box invalide » a disparu d'elle-même. Le plafond de
plausibilité reste : un zéro réel s'affiche « 0,0 kWh », jamais « — ».
hasNeverRun était déjà traité comme un état distinct — « l'absence de cycle est dite pour
elle-même, jamais rendue par un âge de zéro ».
Vérifié sur appareil contre .75 : compteur racine relu, « à configurer » vide, deux bornes
listées (Simulated wallbox + Terra AC Charger), stagesW de la box, les deux champs
writable, 5/5 sections de l'écran SG-Ready. Tests 98/98, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
Je n'avais pas ouvert docs/mockups/ : les écrans ont été construits depuis le texte du brief
seul, et l'écart est franc. Les maquettes du 2026-08-25 font autorité, elles sont désormais
suivies.
ÉCRANS DE MÉCANISME, refaits :
- sélecteur de Things PAR NOM, filtré sur l'interface nymea (power / etmvariableload).
Taper un UUID au doigt est une erreur garantie ET muette : la box accepte un UUID valide
qui ne désigne rien, et la charge ne commande jamais rien. Le sélecteur grise les Things
déjà pris — la box refuse en bloc un même Thing sur deux lignes — et signale en rouge un
ThingId configuré qui n'existe plus sur la box ;
- routeur de relais : ajout/retrait de contacteurs, puissance par ligne, et les paliers
atteignables AFFICHÉS. Ils sont déduits (2^N combinaisons, doublons écartés), jamais
enregistrés — la maquette est explicite là-dessus, ma note précédente disant l'inverse
était une sur-interprétation du brief ;
- SG-Ready : SG1/SG2 se choisissent EN HAUT, une fois, et la table des quatre états devient
éditable — ce qui s'y coche est fermé/ouvert, et c'est ça qui devient relays[]. Les
étiquettes SG1/SG2 sont une convention de l'écran (SG1 = fermé en état 1, SG2 = en état 3)
et elles TOMBENT sur un encodage non standard, où les contacts s'affichent tels quels ;
- modulable : plafond requis > 0, et l'avertissement que changer d'appareil crée une autre
charge puisque l'identifiant EST le ThingId ;
- temporisations minOnS/minOffS (et minStateHoldS pour la PAC) en pas de 30/60 s ;
- « Compteur dédié » présent et GRISÉ, avec sa raison : aucun champ n'existe côté box, et
inventer meterThingId ferait rejeter l'enregistrement de TOUTES les charges ;
- « Inclure dans l'arbitrage » avec rang et domaine, et ce que la désactivation fait
vraiment selon le mécanisme ;
- validation AVANT envoi : relays[] vide, powerW ≤ 0, Thing en double, état 2 absent,
maxPowerW ≤ 0. SetLoadConfig refuse en bloc et sans motif — laisser partir une config
invalide condamne l'installateur à un refus muet qui perd tout l'appel.
LISTE :
- l'en-tête d'arbitrage et le tableau de budget, PERDUS à la fusion, sont rétablis. Sans
eux « Alloué 3 000 W » ne se rattache à rien ;
- le rang passe en pastille près de la poignée, et le sous-titre devient lisible :
« Routeur de relais · 3 relais · min ON 60 s » au lieu de « Routeur de relais · rang 1 » ;
- section « Recharge véhicule » : la borne n'est pas une LoadConfig, elle n'apparaissait donc
NULLE PART — ce qu'un installateur qui vient de la régler lit comme une perte.
MENU : l'entrée « Ordre de service des charges » est retirée ; elle pointait sur une
redirection, donc sur le même écran sous deux noms.
Tests 80/80, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
FUSION. L'écran « Ordre de service » est supprimé ; sa route redirige. C'est son CHEMIN DE
DONNÉES (LoadConfigProvider) qui a migré dans la présentation de « Rôles & appareils »,
jamais l'inverse. EmsRole fige six rôles dont dhw ET heatPump séparés — or LM-201 dit qu'une
PAC sans ballon séparé est UNE charge : le modèle des rôles aurait exigé deux charges sur un
canal unique, ce que validateSet() refuse à raison. Les charges viennent donc de
GetLoadConfig. Les compteurs restent sur EnergySetupProvider (autre contrat, SetRootMeter).
La note « le pilote peut bousculer l'ordre les jours Tempo rouge ou à l'approche d'une
échéance » est retirée : aucun de ces comportements n'existe dans le moteur.
ANNONCE AVANT ÉCRITURE. Miroir exact de EnergyArbitrator::sameHardware(). Le piège, vérifié
sur .75 : la comparaison des relais est INDEXÉE, donc réordonner relays[] sans rien changer
d'autre reconstruit l'adaptateur — contacts ouverts (applySafeState, force) et verrous
réarmés à froid. Un glisser-déposer dans la liste des contacteurs est un geste matériel.
L'écran le nomme, avec la durée de réarmement.
GRISAGE PAR INTROSPECTION. RpcSchema lit JSONRPC.Introspect à la connexion ; trois états et
non deux — écrivable, r: (mesure, pas fonction manquante), absent — plus « inconnu » quand le
schéma n'a pas pu être lu. Chaque champ grisé porte sa raison et le nom du champ manquant.
La version affichée est celle de l'API d'expérience (NymeaEnergy 0.8) : le paquet +etm15
n'est exposé par aucune méthode.
CORRIGÉ AU PASSAGE — les modes de recharge étaient INOPÉRANTS. setChargingInfo() appelait
EnergyPlugin.SetChargingInfo (namespace inexistant) avec mode/targetSoc/endTime/minCurrent
au lieu de chargingMode/targetPercentage/endDateTime, et minCurrent n'existe dans aucun
champ. Vérifié par appel réel : « Missing required key: chargingMode ». Le refus était de
plus avalé et l'état local mis à jour quand même. Désormais l'état ne suit que si la box
accepte, et le refus remonte sans en inventer la cause.
ZONES — constaté, pas corrigé. L'hypothèse « champs grisés » est fausse : AirConditioning
publie 8 méthodes toutes en écriture. ac_screen.dart n'en appelle aucune, ses 4 pièces sont
des constantes de maquette, et .75 déclare zéro zone. getZones() ajouté + bandeau qui lit le
vrai compte et annonce la maquette. Le câblage est un lot en soi, non exerçable sans zone.
Tests 80/80, analyze 0 erreur. Relevé : docs/RELEVE_LOTC.md — dont §6, ce qui n'a PAS pu
être vérifié : l'IU n'a pas pu être pilotée sur l'appareil, MIUI refusant INJECT_EVENTS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff