diff --git a/RECAP.md b/RECAP.md index bb2b9b5..adf8c11 100644 --- a/RECAP.md +++ b/RECAP.md @@ -57,60 +57,123 @@ remplace **tout l'ensemble** → la charge `chauffe-eau` de `.75` aurait été --- -## ⏸️ ÉTAT À LA REPRISE — attendre l'agent plugin +## Reprise du 2026-08-25 (soir) — l'agent plugin a terminé, la moitié télémétrie est faite -**Le lot plugin télémétrie A ÉTÉ déployé sur `.75`** en fin de session (`4ade3e4`) : -**20 méthodes / 14 notifications**, `GetLoadTelemetry`, `LoadTelemetryChanged` et -`o:domain` présents. Les critères **4 à 8** du brief lot B sont donc techniquement -atteignables. +Le blocage est levé : `1.15.2+etm15` (lot plugin **B-bis**) est sur `.75`, `pac-terrain` +est devenue une `LoadConfig` ordinaire, et `GetLoadConfig` / `GetLoadTelemetry` +**concordent** (2 charges des deux côtés). La règle de détection « charge arbitrée hors +configuration » de `+etm14` est **sans objet** — ne pas la réintroduire, et **ne pas +attendre de `configurable: false`** : ce champ n'existe pas. -> **⛔ NE PAS ENCHAÎNER POUR AUTANT.** Décision Patrick, 2026-08-25 : attendre que -> l'agent plugin ait **terminé son travail sur `pac-terrain`** avant de construire la -> moitié télémétrie côté app. -> -> Symptôme observé (reproductible, **travail en cours — pas un bug à remonter**) : -> `GetLoadTelemetry` renvoie **2** charges (`chauffe-eau`, `pac-terrain`) là où -> `GetLoadConfig` n'en renvoie qu'**1** (`chauffe-eau`). Coder l'écran contre cet état -> intermédiaire, c'est coder contre une cible mouvante. +### Livré côté app -### Fixtures capturées avant la bascule (déjà committées) -- `test/fixtures/loadtelemetry_hems75.json` — dump réel de `GetLoadTelemetry` -- `test/fixtures/loadconfig_hems75_v2.json` — `GetLoadConfig` **avec** la clé `domain` -- `test/fixtures/schema_hems75_v2.json` — schémas SET + télémétrie introspectés -- `test/fixtures/loadconfig_hems75.json` — dump d'AVANT, conservé comme référence +| Fichier | Rôle | +|---|---| +| `models/load_telemetry.dart` | Vue en lecture de `GetLoadTelemetry`. `timestamp`/`budget`/`mechanism`/`lock`/`faultCode` **nullables** : omis ≠ zéro. Seuil de fraîcheur `kTelemetryStaleAfter = 180 s`. | +| `services/telemetry_text.dart` | Rendu français des **codes**. 19 motifs, 3 défauts, 3 verrous, 4 mécanismes — et un repli obligatoire. | +| `providers/load_telemetry_provider.dart` | Instantané + `LoadTelemetryChanged`, fraîcheur à **trois signaux**, machine à états de `ClearLoadFault`. | +| `providers/load_config_provider.dart` | `assignDomain()` — affectation en **brouillon**, jamais d'écriture implicite. | +| `screens/energy/load_order_screen.dart` | En-tête budget/fraîcheur/dégradé, bloc télémétrie par charge, encart défaut, menu de domaine. | +| `services/nymea_service.dart` | `getLoadTelemetry()`, `clearLoadFault()`, aiguillage de `LoadTelemetryChanged`. | +| `tools/rpc/{telemetry_watch,domain_assign,fault_probe}.dart` | Sondes permanentes — voir leur `README.md`. | -Forme confirmée de la charge utile (conforme au brief : des **codes**, jamais des phrases) : +**57 tests** (30 avant), `flutter analyze` **25 issues = ligne de base inchangée**. -```json -{ "loadId": "chauffe-eau", "allocatedW": 0, "available": true, - "decision": { "code": "SURPLUS_INSUFFICIENT", "params": { "budgetW": 229 } }, - "lock": { "kind": "minOff", "remainingS": 60 }, - "mechanism": { "kind": "relay", "stageW": 0, "maxStageW": 3500 } } +### Trois choix de conception à ne pas défaire + +1. **La fraîcheur ne repose pas sur le seul `timestamp`.** Trois signaux concourent : + l'âge du `timestamp` (le seul qui vaille à l'instant du premier instantané — un `Get` + rend le dernier cycle *publié*, qui peut dater d'une heure), le temps depuis la + dernière trame (immunisé contre l'écart d'horloge box↔téléphone), et le temps depuis + que le `timestamp` a **cessé d'avancer** — le cas exact que le battement de cœur + existe pour rendre détectable. Le seuil est à **180 s** : trois cycles. Un seuil ≤ 60 s + crierait « données anciennes » sur une installation vivante. +2. **Les secondes d'un verrou ne sont pas décomptées côté app.** Le plugin exclut + volontairement ce champ de sa détection de changement ; un décompte local serait une + extrapolation présentée comme une mesure. L'écran écrit « (au dernier cycle) ». +3. **Un code connu privé d'un paramètre attendu bascule en repli**, comme un code inconnu. + « Consigne de W servie par le surplus » aurait l'air d'une donnée ; le code brut a + l'air de ce qu'il est. + +### Ce que le banc a appris — et qui ne se déduisait pas du code + +**`ClearLoadFault` lève vraiment, et le cycle suivant reverrouille.** Passage du +2026-08-25 18:14 → 18:17 (`tools/rpc/fault_probe.dart`, journal de `.75`) : + +``` +18:14:00 écriture → thing non trouvé → échelle épuisée → charge EN DÉFAUT +18:15:58 [Arbitre] ClearLoadFault → "défaut LEVÉ ; la charge revient à l'arbitrage" + RPC : EnergyErrorNoError +18:16:00 cycle suivant → thing toujours non trouvé → EN DÉFAUT à nouveau ``` -### Acquis à ne pas reperdre -- **L'arbitre n'est PAS figé** : le `timestamp` avance d'une minute par cycle - (`12:37` → `12:38` → `12:39`, vérifié). Le seuil de fraîcheur du §7-6 doit donc être - **supérieur à 60 s** — deux ou trois cycles, pas moins. -- `.75` a désormais une 2e charge (`pac-terrain`, sg-ready) côté télémétrie : le - multi-charges y deviendra exerçable en vrai, contrairement à ce que disait le tableau - ci-dessous. +La télémétrie ne repasse **jamais** à `available: true`. Une app qui aurait annoncé +« défaut levé » sur le retour du RPC aurait menti deux secondes plus tard. C'est la +justification, mesurée, de l'état `pending` maintenu 130 s puis conclu `stillFaulty`. -### Sondes JSON-RPC — désormais permanentes -`tools/rpc/` (voir son `README.md`). `loadconfig_roundtrip.dart` est le **seul qui écrit** -et exige `--yes`. +**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 +(`etmvariableloadadapter.cpp:148-150`, l'ordre des `if`). Ne pas coder l'UI en supposant +qu'un Thing absent produit `THING_MISSING`. + +**`SetLoadConfig` refuse en bloc, et le motif n'est PAS dans la réponse RPC.** La box +renvoie `EnergyErrorInvalidParameter`, point. Le motif — « `etmvariableload fixed : +powerLevels requis (non vide)` », avec le libellé de la charge — est **au journal**. C'est +la « silence de refus » que le relevé du plugin garde ouverte (§9-4) : l'app ne peut pas +expliquer un refus à l'installateur, elle ne peut que le rapporter. + +### Journal de vérification — critères 4 à 8 + +| Critère | Verdict | +|---|---| +| **§7-4 domaine** | ✅ **sur `.75`** — écrit par le code d'écriture de l'app (`domain_assign.dart` importe `patched()`/`flattenToPayload()`), relu identique, persisté dans `/var/lib/nymea/energy-load-configuration.json` (`domain='ecs'` / `'hvac'`). ⚠️ **le redémarrage de `nymead` n'a PAS été exercé** : `sudo` demande un mot de passe sur `.75`. | +| **§7-5 télémétrie vivante** | ✅ **sur `.75`** — trames non sollicitées, allocation 500 → 3000 → 3500 W, `SG_NORMAL` → `LOCK_MIN_STATE_HOLD`, verrou 241 → 180 → 120 s. | +| **§7-6 fraîcheur** | ✅ **protocole mesuré sur `.75`** (battement ~59 s, cycle qui avance d'une minute) ; ⚠️ la **bascule d'affichage** est éprouvée en test unitaire, pas à l'écran — l'app n'a jamais tourné sur appareil (voir §7-9). | +| **§7-7 défaut** | ✅ **sur `.75`** — défaut provoqué, `available: false`, `faultCode` rendu en français, `ClearLoadFault` exercé et son inefficacité **constatée sur la télémétrie**. ⚠️ pas de fixture brute : le dump n'a pas été conservé (`--dump` le capturera au prochain passage). | +| **§7-8 code inconnu** | ✅ test unitaire — code absent de l'ARB **et** code connu privé d'un paramètre. Non exerçable sur `.75` : la box n'émet que des codes du catalogue. | +| **§7-1bis ECS-412** | ✅ **levé** grâce au ssh — journal de l'écriture de domaine : `0 créée(s), 0 mise(s) à jour en place, 2 inchangée(s), 0 retirée(s)`. Aucune reconstruction d'adaptateurs. | + +### État du banc à la fin de la session + +`.75` est **laissé classé** : `chauffe-eau` → `ecs`, `pac-terrain` → `hvac`. Le `domain` +est une pure métadonnée que l'arbitre ne lit pas, et cet état rend enfin le multi-domaines +exerçable sur la machine. Pour revenir en arrière : + +```bash +dart run tools/rpc/domain_assign.dart 192.168.1.75 --clear --yes +``` + +La charge fictive de la sonde de défaut a été **retirée**, et la configuration relue est +identique clé par clé à celle lue avant la sonde. ## Vérifié / non vérifié (lot B app) | Critère | Statut | |---|---| | §7-1 aller-retour neutre | ✅ **sur `.75` en vrai** — `SetLoadConfig` verbatim → relecture identique | -| §7-1bis non-rebuild ECS-412 | ⚠️ lecture de code seulement (`sameHardware()`), journal non consulté | -| §7-2 aplatissement réversible | ✅ **test unitaire seul** — `.75` n'a qu'une charge | +| §7-1bis non-rebuild ECS-412 | ✅ **sur `.75`** — journal lu : 2 charges inchangées, 0 reconstruite | +| §7-2 aplatissement réversible | ✅ **test unitaire seul** — `.75` n'a que deux charges, désormais dans deux domaines | | §7-3 config non contiguë | ✅ **test unitaire seul** — fixture synthétique | -| §7-4 à §7-8 | ❌ bloqués (lot plugin) | +| §7-4 domaine | ✅ **sur `.75`** ; ⚠️ redémarrage `nymead` non exercé (sudo verrouillé) | +| §7-5 télémétrie vivante | ✅ **sur `.75`** | +| §7-6 fraîcheur | ✅ protocole sur `.75` ; ⚠️ bascule d'affichage en test unitaire seul | +| §7-7 défaut | ✅ **sur `.75`** (sonde `fault_probe.dart`) | +| §7-8 code inconnu | ✅ test unitaire (non exerçable sur une box conforme) | | §7-9 locale | ✅ protocole vérifié sur `.75` ; ⚠️ **chemin app jamais exécuté sur appareil** (adb refusé) | -| §7-10 analyze / tests / gating | ✅ 25 issues = ligne de base, 30/30 tests | +| §7-10 analyze / tests / gating | ✅ 25 issues = ligne de base, 57/57 tests | + +## Ce qui reste à faire sur ce lot + +1. **Redémarrer `nymead` sur `.75`** et relire la configuration — dernier morceau du §7-4. + `sudo systemctl restart nymead` demande un mot de passe que l'agent n'a pas. +2. **Faire tourner l'app sur appareil** (`adb` refusé jusqu'ici). Tant que ça n'est pas + fait, §7-6 et §7-9 restent des vérifications de protocole, pas d'écran. +3. **Capturer une fixture brute de défaut** au prochain passage de `fault_probe.dart` + (`--dump`). +4. **Écran de réglage par charge** — l'agent plugin le demande explicitement : + `pac-terrain` est désormais renommable, classable, rangeable, désactivable. Le brief du + lot B limitait l'écriture à `priority` + `o:domain` ; renommer et désactiver sont + **hors de ce lot**, et attendent un arbitrage. ## Question ouverte à trancher @@ -119,13 +182,6 @@ est bâti sur le modèle **inverse** (il construit des `LoadDescriptor` depuis d J'ai créé `/energy/loads` à côté et laissé `/energy/setup` intact. **Fusionner les deux est une refonte, pas un câblage** — décision Patrick attendue. -## Nouveauté à exploiter à la reprise - -`CLAUDE.md` documente désormais l'accès banc : `ssh etm@192.168.1.75` (clé installée), -journal via `journalctl -u nymead`. **Cela lève la limite qui rendait §7-1bis invérifiable** -— au prochain aller-retour neutre, lire le journal pour confirmer qu'aucune reconstruction -d'adaptateurs n'a lieu. - --- # RECAP — etm-powersync-app · reprise de session diff --git a/tools/rpc/README.md b/tools/rpc/README.md index 7503650..331d093 100644 --- a/tools/rpc/README.md +++ b/tools/rpc/README.md @@ -22,6 +22,9 @@ dart tools/rpc/