Patrick Schurig 40ea285663 tools(rpc): sondes télémétrie / domaine / défaut, et journal de vérification
Trois sondes permanentes, et le relevé de ce qu'elles ont montré sur .75.

- telemetry_watch.dart (lecture seule) : cadence du battement de cœur et avancée
  du cycle. C'est la seule mesure qui distingue une installation stable d'un
  moteur arrêté — les deux produisent le même flux de trames.
- domain_assign.dart (écrit, --yes) : classe des charges en important le code
  d'écriture de l'app. Une sonde qui sérialiserait à sa façon ne prouverait rien
  sur l'app.
- fault_probe.dart (écrit, --yes) : provoque une charge en défaut, éprouve
  ClearLoadFault, restaure. La config d'origine part sur le disque AVANT la
  première écriture et la restauration rejoue ce fichier verbatim — filtrer la
  sonde hors d'une relecture serait une reconstruction, c'est-à-dire le chemin
  qui a failli écraser la config du banc via persist(). La config relue est
  ensuite comparée clé par clé, pas seulement « la sonde a disparu ».

Ce que le banc a appris, et qui ne se déduisait pas du code :

- ClearLoadFault lève RÉELLEMENT le verrou (journal : « défaut LEVÉ ») et le
  cycle suivant reverrouille, la cause n'ayant pas disparu. La télémétrie ne
  repasse jamais à available:true. Une app qui aurait cru l'acquittement aurait
  menti deux secondes plus tard.
- Le défaut publié est WRITE_FAILED, pas THING_MISSING : l'écriture est tentée,
  échoue, l'échelle s'épuise, et m_faulted masque le second code.
- SetLoadConfig refuse en bloc et le motif n'est PAS dans la réponse RPC — il est
  au journal. L'app ne peut que rapporter un refus, pas l'expliquer.
- ECS-412 confirmé au journal : une écriture de domaine ne reconstruit aucun
  adaptateur (« 2 inchangée(s), 0 retirée(s) »). Le §7-1bis n'est plus une
  lecture de code.

.75 est laissé classé (chauffe-eau→ecs, pac-terrain→hvac) : métadonnée que
l'arbitre ne lit pas, et le multi-domaines y devient exerçable. Retour arrière :
domain_assign.dart --clear --yes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-25 19:20:46 +02:00

146 lines
7.0 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 |
| `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** |
### `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.
### `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. |