Patrick Schurig 771345f1bc tools(rpc): une sonde qui dit ce que l'app AFFICHERAIT de measurement
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
2026-08-28 15:32:52 +02:00

9.5 KiB

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 :

  1. 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.
  2. 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().
  3. 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, pas THING_MISSING — l'écriture est tentée, échoue faute de Thing, l'échelle s'épuise, et m_faulted masque le second code ;
  • ClearLoadFault lève réellement le verrou (le journal le dit : « défaut LEVÉ ; la charge revient à l'arbitrage »), le RPC répond EnergyErrorNoError — 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.