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
189 lines
9.5 KiB
Markdown
189 lines
9.5 KiB
Markdown
# 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.
|
|
|
|
```bash
|
|
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`
|
|
|
|
```bash
|
|
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`
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```bash
|
|
ssh etm@192.168.1.75 'journalctl -u nymead -f'
|
|
```
|
|
|
|
### `telemetry_watch.dart`
|
|
|
|
```bash
|
|
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`
|
|
|
|
```bash
|
|
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`
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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. |
|