Le piège des accolades n'était pas un détail de chargingModeOf(). C'est la deuxième
occurrence en une semaine d'un motif qui se répétera : deux appels JSON-RPC de la même box,
portant le même objet, n'emploient pas la même convention, et RIEN ne le dit. Écrit comme
tel dans docs/UI_data_contract.md §9, avec renvoi depuis les deux sites de code et depuis
tools/rpc/README.md — là où quelqu'un croise deux réponses brutes.
Les occurrences, leur symptôme :
· GetPowerBalance (currentPowerProduction…) × GetPowerBalanceLogs (production…) →
tout tombe à zéro, l'historique paraît vide sur 7 960 échantillons bien présents
· GetChargingInfos (evChargerId sans accolades) × GetLoadConfig (id avec) →
« mode non lu » sur une borne parfaitement lue, et la note « hors arbitrage » muette
· famille voisine, même silence : currentPower* en W × total* en kWh, facteur 1000
Ce qui en fait un motif et pas trois anecdotes : le symptôme n'est JAMAIS une exception,
c'est une valeur neutre plausible, et un test unitaire qui construit ses deux côtés avec la
même convention passe aussi. D'où les trois gestes écrits au §9 — sonder les deux appels
côte à côte sur les charges utiles brutes, éprouver le rapprochement sur les deux formes
réelles, et se méfier d'un vide qui ne coûte rien à expliquer.
Et le lien avec une règle qu'on suivait déjà sans la relier à ça : une valeur neutre issue
d'un croisement raté est indiscernable d'une vraie absence. « Non lu » laisse une chance de
trouver le bug ; « 0 » l'enterre.
── Campagne V2C arrêtée, et la réserve sessionEnergy avec elle ────────────────
La Trydan non-Pro est en WiFi ESP32 sans repli filaire, lien inexploitable à deux mètres du
point d'accès : 70 % de scrutations avortées, 12 bascules en injoignable en vingt minutes.
Le modèle sort du catalogue ETM ; seule la Trydan Pro (port Ethernet) y reste.
La trace qui lève C1 — chargeEnergy à zéro AVEC un sessionEnergy qui continue — n'a donc
pas été prise et ne viendra pas de cette borne. Les notes restent en place telles quelles,
avec la raison mise à jour : elles seront levées sur une Trydan Pro. Aucun avancement de
charge affiché d'ici là.
164 tests, flutter analyze inchangé à 27 remarques.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
172 lines
8.5 KiB
Markdown
172 lines
8.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 |
|
|
| `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`.
|
|
|
|
### `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. |
|