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
Sondes JSON-RPC — tools/rpc/
Scripts dart:io autonomes pour interroger une box nymea sans lancer l'app.
Pourquoi ils existent : sur ce poste, Flutter Linux natif est bloqué (snap sans linker
ld.lld) et le web ne peut pas joindre le LAN en ws:// (CORS). Ces sondes sont donc le
moyen le plus court de vérifier un comportement réel de la box — et elles font plus autorité
qu'une lecture de code ou qu'un Introspect recopié de mémoire.
Aucune dépendance hors SDK : dart:io + dart:convert. Rien à installer.
dart tools/rpc/<script>.dart [host] [args…] # host par défaut : 192.168.1.75
Les scripts
| Script | Ce qu'il fait | Écrit ? |
|---|---|---|
probe.dart |
Un appel RPC quelconque, réponse en JSON indenté. Le couteau suisse. | non |
notifications.dart |
S'abonne à tous les namespaces, compte et échantillonne ce qui arrive. | non |
locale.dart |
Comportement de o:locale : connexions simultanées, re-Hello, locale absurde. |
non |
locale_diff.dart |
Aplatit toutes les chaînes affichables et les diffe entre locales. | non |
loadconfig_roundtrip.dart |
Aller-retour neutre Get → Set verbatim → Get. |
⚠️ OUI |
telemetry_watch.dart |
Instantané puis écoute de LoadTelemetryChanged : cadence, avancée du cycle, allocations. |
non |
evcharger_watch.dart |
Suit les états d'une borne (branchement, charge, énergie) — y compris quand elle est hors arbitrage. | non |
measurement_check.dart |
Dit, charge par charge, ce que l'app afficherait de measurement — via le vrai modèle. |
non |
domain_assign.dart |
Classe des charges par o:domain, par le code d'écriture de l'app. |
⚠️ OUI |
fault_probe.dart |
Provoque une charge en défaut, éprouve ClearLoadFault, restaure. |
⚠️ OUI |
⚠️ Avant de rapprocher deux réponses. Deux appels de la même box n'emploient pas forcément la même convention — clés de champs, graphie des identifiants, unités — et rien ne le signale : le croisement raté rend un neutre plausible, jamais une erreur. Le motif, ses occurrences connues et la façon de le chercher sont dans
docs/UI_data_contract.md§9. C'est ici, la sonde en main, qu'il se voit le plus vite : comparer les charges utiles brutes des deux appels, jamais les modèles Dart.
probe.dart
dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.GetLoadConfig
dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect > introspect.json
dart tools/rpc/probe.dart 192.168.1.75 Integrations.GetThings
dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.GetChargingInfos '{"evChargerId":"…"}'
Le Hello part toujours en premier et son contenu va sur stderr, la réponse sur
stdout — d'où le > fichier.json propre ci-dessus.
notifications.dart
dart tools/rpc/notifications.dart 192.168.1.75 90 # écoute 90 s
Utile pour répondre à « est-ce que cette information franchit la frontière RPC ? ». Un namespace qui ne produit rien pendant que le système travaille est une réponse, pas un silence à interpréter.
locale.dart / locale_diff.dart
locale.dart montre que nymea gère la locale par connexion (deux sockets, deux
langues). locale_diff.dart répond à la question qui compte vraiment : est-ce que ça
change quelque chose ? Relevé du 2026-08-25 sur .75 — sur 784 chaînes affichables,
de_DE en change 1, fr_FR/it_IT/es_ES/nl_NL zéro.
loadconfig_roundtrip.dart — ⚠️ écrit
dart tools/rpc/loadconfig_roundtrip.dart 192.168.1.75 --yes
SetLoadConfig remplace l'ensemble des charges. Le contenu renvoyé est identique à ce
qui vient d'être lu, donc l'opération est neutre — mais elle reste une écriture sur une
installation vivante, et elle déclenche une réévaluation des adaptateurs. Le drapeau
--yes est obligatoire pour qu'elle ne parte jamais par réflexe de flèche haut.
Pour confirmer qu'aucune reconstruction d'adaptateurs n'a lieu (ECS-412, m_builtFrom +
sameHardware()), lire le journal de la box en parallèle :
ssh etm@192.168.1.75 'journalctl -u nymead -f'
telemetry_watch.dart
dart tools/rpc/telemetry_watch.dart 192.168.1.75 240 # écoute 240 s
Répond à deux questions que la lecture de code ne tranche pas : une trame arrive-t-elle
sans qu'on la demande, et le timestamp avance-t-il ? La seconde est la seule qui
distingue une installation stable d'un moteur arrêté — les deux produisent le même flux de
battements de cœur. Relevé du 2026-08-25 sur .75 : battement à ~59 s, cycle qui avance
d'une minute, et une trame de changement significatif dans la foulée.
evcharger_watch.dart
dart tools/rpc/evcharger_watch.dart 192.168.1.75 600 V2C # 600 s, bornes dont le nom contient « V2C »
telemetry_watch.dart ne montre une borne que si elle est arbitrée ; sans véhicule
branché elle n'entre pas dans loads[] (depuis +etm24), et c'est justement pendant ces
transitions qu'il faut voir ce que le Thing publie. Cette sonde s'abonne aux états du Thing
et n'imprime que ce qui change.
Relevé du 2026-08-28 sur la V2C Trydan de .75 — les deux constats qui bloquent LM-1009 :
chargeEnergy est exact pendant la charge (0,3456 kWh, croissance qui recoupe
currentPower à mieux d'un pour cent) mais remis à zéro à l'arrêt de la charge, 119 s
avant que le câble ne bouge ; et powerL1/L2/L3 sont des ampères mal nommés. Aucun des
deux ne doit être affiché tant que le plugin n'a pas publié sessionEnergy et le renommage
currentL1/L2/L3.
measurement_check.dart
dart run tools/rpc/measurement_check.dart 192.168.1.75
⚠️ dart run : la sonde importe le modèle de l'app (LoadTelemetryEntry.fromJson)
plutôt que de relire le JSON à sa façon — une lecture parallèle ne prouverait rien sur ce
que l'écran fera. Elle rend, par charge, la source publiée et la phrase que l'app en tire.
Ce qu'elle sert à voir, et 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 n'est plus publié, chauffe-eau et pac-terrain
sont en meter (1 500 W et 800 W), la V2C est absente — pas none.
domain_assign.dart — ⚠️ écrit
dart run tools/rpc/domain_assign.dart 192.168.1.75 --set chauffe-eau=ecs --set pac-terrain=hvac --yes
dart run tools/rpc/domain_assign.dart 192.168.1.75 --clear --yes # tout déclasser
⚠️ dart run et non dart : ce script importe le code de l'app
(LoadConfigEntry.patched(), groupByDomain(), flattenToPayload()) plutôt que de
fabriquer sa propre charge utile. Une sonde qui sérialiserait à sa façon ne prouverait rien
sur l'app. Le domain est une pure métadonnée — l'arbitre ne la lit pas — mais l'écriture
est réelle.
fault_probe.dart — ⚠️ écrit, et ajoute une charge fictive
dart run tools/rpc/fault_probe.dart 192.168.1.75 --yes
dart run tools/rpc/fault_probe.dart 192.168.1.75 --yes --dump fixtures_defaut
dart run tools/rpc/fault_probe.dart 192.168.1.75 --restore-only --yes
Ajoute une charge etmvariableload dont le ThingId n'existe pas, attend deux cycles,
observe la télémétrie, appelle ClearLoadFault, puis restaure verbatim la
configuration lue au départ.
Trois garde-fous, tous délibérés :
- La configuration d'origine part sur le disque AVANT la première écriture
(
fault_probe_backup_<host>.json, git-ignoré). Un plantage à mi-parcours ne doit pas laisser le banc avec la charge fictive et aucune référence de retour. - La restauration rejoue ce fichier verbatim, jamais une relecture filtrée. Retirer
la sonde d'une config relue serait une reconstruction — le chemin exact qui a failli
écraser la configuration du banc via
EnergySetupProvider.persist(). - Après restauration, la config relue est comparée clé par clé à celle du départ, pas seulement « la sonde a disparu ». C'est le critère 1 appliqué ici.
Le label de la charge fictive est SONDE-TEST-A-SUPPRIMER : si elle survit à un incident,
personne n'aura à se demander d'où elle sort.
Ce que le passage du 2026-08-25 a montré, et qui ne se déduisait pas du code :
- le défaut publié est
WRITE_FAILED, pasTHING_MISSING— l'écriture est tentée, échoue faute de Thing, l'échelle s'épuise, etm_faultedmasque le second code ; ClearLoadFaultlève réellement le verrou (le journal le dit : « défaut LEVÉ ; la charge revient à l'arbitrage »), le RPC répondEnergyErrorNoError— et le cycle suivant reverrouille, la cause n'ayant pas disparu. La télémétrie ne repasse jamais àavailable: true.
C'est la démonstration la plus nette de la règle du lot : l'acquittement d'un RPC ne vaut pas application. Une app qui aurait affiché « défaut levé » sur le retour du RPC aurait menti deux secondes plus tard.
Bancs
| Cible | Particularité |
|---|---|
192.168.1.75 |
ouverte (authenticationRequired: false) — aucune auth à gérer dans les sondes. Deux charges depuis le plugin 1.15.2+etm15 : chauffe-eau (relay-router, ecs) et pac-terrain (sg-ready, hvac). |
192.168.1.120 |
auth requise — ces sondes ne gèrent PAS Authenticate. À étendre avant usage. |