Établi le 2026-09-06 : deux tentatives de réordonnancement par `input swipe` puis `input motionevent`, aucun mouvement — et la conclusion « le glisser-déposer est cassé » aurait été fausse. Le même geste au doigt, le matin même, avait produit une écriture qui a tenu. `input swipe` interpole une trajectoire linéaire ; un appui-glissé réel comporte une phase de maintien que Flutter attend avant d'armer le glissement. Ce qui rend le piège coûteux est que l'échec RESSEMBLE à un défaut : la poignée capte le geste — la liste ne défile pas, ce qu'elle ferait s'il passait à travers — et rien ne bouge. Un réordonnancement se vérifie donc au doigt, ou pas du tout ; ce qu'`adb` peut vérifier, c'est le résultat relu dans `GetLoadConfig`. Même famille que le `finally` qui défait l'action au lieu de restaurer l'état : l'outil répond, et sa réponse ne parle pas de ce qu'on croit mesurer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
269 lines
15 KiB
Markdown
269 lines
15 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** |
|
|
|
|
> ⚠️ **Une sonde écrite vite retombe dans les pièges que l'app évite.** Deux payés dans la
|
|
> même session, le 2026-08-29, dans des scripts jetables de vingt lignes :
|
|
>
|
|
> 1. **Un refus arrive en `status: "success"`.** Le verdict est dans `params.energyError` —
|
|
> `EnergyErrorInvalidParameter` pour un rejet, `EnergyErrorNoError` pour un succès. Une
|
|
> sonde qui ne teste que `status` annonce « accepté » sur une écriture que la box vient
|
|
> de rejeter, et c'est le journal de la box qui rétablit les faits. `NymeaService`
|
|
> teste bien les deux ; la sonde, non.
|
|
> 2. **Les identifiants ne s'écrivent pas pareil d'un appel à l'autre** — `{uuid}` en
|
|
> configuration, `uuid` nu côté Thing. Le §9 du contrat de données le documente **depuis
|
|
> la veille**, et la sonde est quand même tombée dedans : la comparaison n'a jamais
|
|
> trouvé sa cible, et le script a rendu des verdicts sur une charge qu'il 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** : elle relit le JSON à sa façon, donc elle refait
|
|
> les erreurs que le modèle a déjà résolues. Quand la question porte sur ce que l'app fera,
|
|
> importer le modèle de l'app (`dart run`, cf. `measurement_check.dart`) vaut mieux que le
|
|
> relire — et quand la question porte sur la box, croiser la réponse RPC avec
|
|
> `journalctl -u nymead` 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 : la box dit *« la charge "Simulated wallbox 2" prétend un rankOrigin "auto"
|
|
> que le moteur n'a pas posé »*, l'app reçoit `EnergyErrorInvalidParameter`. C'est le
|
|
> silence de refus ouvert depuis le premier jour, rencontré une fois de plus — et la raison
|
|
> pour laquelle chaque miroir client (`validateSet`) vaut le coût de sa maintenance.
|
|
|
|
> ⚠️ **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.
|
|
|
|
> ⚠️ **Défaire son action n'est pas restaurer l'état.** *(Payé le 2026-08-30, en cherchant à
|
|
> provoquer `EV_GRID_START` sur la wallbox simulée.)*
|
|
>
|
|
> La sonde branchait la borne par `Integrations.ExecuteAction`, surveillait, et appelait
|
|
> `disconnect` dans un `finally`. Trois choses fausses là-dedans, et **aucune n'a produit
|
|
> d'erreur** :
|
|
>
|
|
> 1. **`connect` ne fait pas ce que son nom promet.** Il bascule `connected` — la liaison du
|
|
> Thing — et **pas `pluggedIn`**, qui est en lecture seule et qu'**aucune action n'écrit**.
|
|
> La borne n'est donc jamais entrée dans `loads[]`, et le motif cherché était inatteignable
|
|
> par ce chemin. 18 cycles pour l'apprendre.
|
|
> 2. **Le `finally` défaisait le GESTE, pas l'ÉTAT.** `connected` valait **déjà `true`** avant
|
|
> la sonde : le `disconnect` « de restauration » a laissé la borne déconnectée dix minutes.
|
|
> Un `finally` symétrique de l'action n'est juste que si l'action est un interrupteur dont
|
|
> on connaît la position de départ — c'est-à-dire jamais, sauf à l'avoir relevée.
|
|
> 3. **`ThingErrorNoError` est un VRAI succès**, et c'est ce qui rend ce cas plus vicieux que
|
|
> le `status: "success"` du bloc précédent : l'action **a** réussi, elle ne faisait pas ce
|
|
> qu'on croyait. Aucun code d'erreur ne dira jamais « tu t'es trompé d'action ». **Le seul
|
|
> verdict est l'état relu.**
|
|
>
|
|
> **Règle, et `fault_probe.dart` la tenait déjà pour la configuration** : capturer l'état
|
|
> **avant** la première écriture, restaurer **vers la capture** et non vers l'inverse du
|
|
> geste, puis **comparer état par état** — le script de retour lit le fichier de capture,
|
|
> jamais la mémoire de ce qu'on a tapé. Ce qui valait pour `SetLoadConfig` vaut identiquement
|
|
> pour un `ExecuteAction`.
|
|
>
|
|
> **Et ce que le passage a mesuré, pour qui réessaiera** : `EV_GRID_START` n'est pas
|
|
> provocable depuis la frontière RPC. Il faudrait une prise véhicule (aucune action ne
|
|
> l'expose) **et** une voiture assignée — les `ChargingInfos` du banc n'ont aucun
|
|
> `assignedCarId`, et sans elle la borne reste hors `loads[]` même branchée (`+etm24`). En
|
|
> revanche la **fenêtre** s'ouvre bien toute seule : le 2026-08-30 à 07:46, surplus à 905 W,
|
|
> entre zéro et le minimum de la borne (~1 380 W à 6 A monophasé). Ce n'est pas la fenêtre qui
|
|
> manque, c'est la borne dans l'arbitrage.
|
|
|
|
> ⚠️ **L'entrée synthétique ne reproduit pas un appui-glissé.** *(Établi le 2026-09-06.)*
|
|
>
|
|
> `adb shell input tap` et `input swipe` pilotent très bien l'app — navigation, saisie du PIN,
|
|
> défilement des listes. Mais **un réordonnancement par poignée ne se vérifie pas ainsi** :
|
|
> `input swipe` interpole une trajectoire linéaire, là où un vrai appui-glissé comporte une
|
|
> **phase de maintien** que Flutter attend avant d'armer le glissement. `input motionevent`
|
|
> DOWN/MOVE/UP n'y suffit pas davantage.
|
|
>
|
|
> Ce qui rend le piège coûteux, c'est que **l'échec ressemble à un défaut** : la poignée capte
|
|
> le geste — la liste ne défile pas, ce qu'elle ferait si le geste passait à travers — et rien
|
|
> ne bouge. On conclut « le glisser-déposer est cassé », et il ne l'est pas : le même geste
|
|
> **au doigt** produit une écriture qui tient.
|
|
>
|
|
> **Donc : un réordonnancement se vérifie au doigt, ou pas du tout.** Ce qui se vérifie par
|
|
> `adb`, c'est le RÉSULTAT — l'ordre relu dans `GetLoadConfig` après le geste de quelqu'un.
|
|
>
|
|
> Même famille que le `finally` qui défait l'action au lieu de restaurer l'état : l'outil
|
|
> répond, et sa réponse ne parle pas de ce qu'on croit mesurer.
|
|
|
|
### `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. |
|