# RECAP — etm-powersync-app · reprise de session > **Session 2026-08-25.** Branche **`feature/beta-add-config`**. Rien n'est poussé. > La section « État au 2026-06-30 » plus bas reste valable pour tout ce qu'elle décrit. --- ## Session 2026-08-25 — ce qui a été fait ### Commits ajoutés | Commit | Contenu | |---|---| | `7ca4297` | **Lot B app** — ordre de service des charges, locale, ossature i18n | | `40ac3b2` | Fixtures : dump réel `GetLoadConfig` + schéma introspecté de `.75` | ### 1. Menus (demande initiale) - « Rôles & appareils » et « Options développeur » → **section MODE INSTALLATEUR** uniquement. - Groupes / Scènes / Médias / Garages → **masqués par défaut**. Le drawer lit désormais `AppSettingsProvider.visibleScreens` (ces 4 écrans y étaient **déjà** à `visible: false` ; le drawer les affichait en dur et ignorait le réglage). - Garde de routeur `_isInstallerRoute` (`main.dart:41`) : `/settings/system*`, `/settings/app/developer`, `/energy/setup`, `/energy/loads` → redirection vers `/` si le mode installateur est verrouillé. Couvre l'URL directe, le deep-link et l'auto-lock 10 min. - Liens SUPPORT corrigés : doc → `https://docs.etm-powersync.fr/`, Telegram → `https://t.me/etm_powersync_support`, **Discord retiré** (commenté sur place). ### 2. Investigations rendues (constats factuels, pas de code) - **Locale nymea** : gérée **PAR CONNEXION** (`o:locale` dans `Hello`, `m_clientLocales`). Pas de forçage `LANG`/`LC_ALL` dans l'unité systemd. **Mais** sur 784 chaînes affichables de `.75`, l'allemand en change **1**, le français **0** : aucun catalogue `.qm` fr n'est livré. Les `displayName` = identifiants anglais. - **Frontière RPC / runtime de l'arbitre** : quasi rien ne sort. Seuls `degradedMode` (notification `ChargingSchedulesChanged` **uniquement** — pas relisable par `Get`), les deux ratios d'énergie, `chargingState` de l'EV et le plan de recharge franchissent. Allocation, budget, verrous, paliers, `available`, motifs : **journal seulement**. `SurplusContext` / `LoadContextTelemetry` n'ont aucune sérialisation sortante. ### 3. Lot B app — livré - `models/load_config_entry.dart` — **vue en lecture** au-dessus de la map brute ; écriture par `patched()` depuis la map d'origine, jamais de re-sérialisation. - `services/load_priority.dart` — `groupByDomain` / `flattenToPayload` / `applyDefaultDomainOrder` / miroir client de `validateSet()`. - `providers/load_config_provider.dart` — séquence `pending → LoadConfigChanged → confirmed|diverged`. Ne conclut jamais sur l'acquittement du RPC. - `screens/energy/load_order_screen.dart` — route `/energy/loads`, gatée installateur. - i18n : `flutter_localizations` + `intl` + `lib/l10n/app_fr.arb` + `l10n.yaml`. Extension `context.l10n`. Français seul. - `NymeaEnergy.GetLoadConfig` / `SetLoadConfig` deviennent **réels** (c'étaient des stubs). - **Locale** envoyée à chaque `Hello`, demandée + retenue journalisées. ### 4. ⚠️ Piège désamorcé — à ne pas réintroduire `EnergySetupProvider.persist()` appelait `setLoadConfig(buildLoadDescriptors())`, une **reconstruction depuis le modèle typé**, et `_commit()` l'appelle à chaque changement d'UI. Inoffensif tant que le RPC était un stub ; destructeur une fois réel (`SetLoadConfig` remplace **tout l'ensemble** → la charge `chauffe-eau` de `.75` aurait été écrasée). **Émission débranchée**, raison documentée sur place. --- ## ⏸️ ÉTAT À LA REPRISE — attendre l'agent plugin **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. > **⛔ 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. ### 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 Forme confirmée de la charge utile (conforme au brief : des **codes**, jamais des phrases) : ```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 } } ``` ### 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. ### Sondes JSON-RPC — désormais permanentes `tools/rpc/` (voir son `README.md`). `loadconfig_roundtrip.dart` est le **seul qui écrit** et exige `--yes`. ## 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-3 config non contiguë | ✅ **test unitaire seul** — fixture synthétique | | §7-4 à §7-8 | ❌ bloqués (lot plugin) | | §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 | ## Question ouverte à trancher Le brief désignait « Rôles & appareils » (`/energy/setup`) comme écran cible, mais cet écran est bâti sur le modèle **inverse** (il construit des `LoadDescriptor` depuis des `EmsRole`). 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 > État au 2026-06-30. Branche **`feature/beta-add-config`** (9 commits au-dessus de `master`). > **Rien n'est poussé** (push différé). **Aucun merge** vers master/silo. --- ## 1. Où on en est (commits, du plus récent au plus ancien) | Commit | Contenu | |---|---| | `b4a6c97` | **Lot C — squelette discovery** de things (⚠️ *pending SDM réel*) | | `b7a7213` | Catalogue de things (lecture seule) + filtres (lot A) | | `3a535e0` | Écran **Protocoles** — CRUD masters Modbus RTU | | `0ebbde4` | chore(docs) — specs/contrats/refs rangés dans `docs/` | | `be6694c` | chore — ignore `/screenshots` | | `f755c72` | docs — contrat UI **rev.5** (réserve décision-2 levée) | | `3821bf3` | refactor(energy) — retrait calcul ratios app-side → seam `EnergyRatiosInterim` | | `b5abe42` | docs — contrat UI rev.4 | | `c638ec6` | **Connexions multi-HEMS + Rôles & appareils** (gros commit fondateur) | **Source de vérité contrats** : `docs/UI_data_contract.md` (rev.5), `docs/reference/jsonRPC.txt` (introspection — fait foi pour les RPC). --- ## 2. Ce qui est LIVRÉ et fonctionnel ### Connexions multi-HEMS (validé live) - `models/installation.dart`, `services/installation_store.dart` (token **par UUID** via `flutter_secure_storage`, métadonnées SharedPreferences). - `services/connection_manager.dart` : mono-connexion, `switchTo`, `connectNew`, `enterDemo` (démo = active factice), contrôle d'identité DHCP, invalidation des caches au switch. - `nymea_service` : `connect()` réveillé (ws://4444, token par UUID), capture Hello (uuid/name/initialSetupRequired), `authenticate`/`createUser`, `resetState`. - Écran **Installations** (3 états) + écran **Connexion** (auth 2 branches, détection auto). - **Gate de routage** (`buildAppRouter`, redirect + `refreshListenable: merge([cm,svc])`), `hasActiveConnection = isSimulation || (connected && authenticated)`. - En-tête drawer cliquable → Installations. - **Testé en vrai** : login auth contre `.120`, `.75` ouvert, switch, déverrouillage installateur. ### Rôles & appareils - `EnergySetupProvider` tri-état (present/absent/non-configuré), écran `roles_devices_screen`, `LoadDescriptor` (rév.2 : PAC exclue → SG-Ready), `Energy.SetRootMeter` réel, Get/SetLoadConfig en **stub loggé**. ### Energy — Phase 1 (interim) - `services/energy_ratios.dart` = **SEAM UNIQUE** `EnergyRatiosInterim` : ratios autoconso/autonomie par Δ-de-cumuls (reseed jour/non-monotone, gardes n/a, clamp). `selfConsumptionPower` net-signé **sans `.abs()`**. Test : `test/energy_ratios_test.dart`. - Calcul ratios app-side **retiré** de `_parsePowerBalance` + getter `autoconsommationW` supprimé. ### Installateur — Modbus RTU (validé live) - `models/modbus_models.dart`, 5 RPC `ModbusRtu.*` dans `nymea_service`, écran `protocols_screen` (CRUD), route `/settings/system/protocols`. ### Installateur — Catalogue things lot A (validé live) - `getVendors()` + `getAllThingClasses()`, écran `thing_catalog_screen` (liste groupée + 3 filtres : fabricant/type/recherche + détail read-only), route `/settings/system/things`. ### Installateur — Discovery lot C (SQUELETTE, NON validé matériel) - `ThingDescriptor`, `discoverThings()`, `addThingFromDescriptor()`, `thing_discovery_screen` (form slaveAddress → scan → descriptors → AddThing). --- ## 3. ⚠️ BLOQUEURS / EN ATTENTE - **Lot C — merge bloqué** tant qu'on n'a pas un **descriptor SDM réel** sur le bus de hems `.75` (besoin : adaptateur USB-RS485 + un vrai compteur SDM branché ; aujourd'hui le master `/dev/ttyUSB0` est `connected:false`). **3 points marqués `À REVALIDER SUR SDM RÉEL`** dans `thing_discovery_screen.dart` / `nymea_service.dart` : 1. résolution master/slave par **heuristique de nom** de paramTypes (`'master'`/`'modbus'`/`'slave'`) ; 2. **timeout** scan (`_sendRequest` = 15 s, à allonger si bus réel plus lent) ; 3. `title`/`description` du descriptor (peuvent être vides/génériques). - **Push** : jamais fait (volontaire). --- ## 4. Lots / phases NON FAITS (file d'attente) - **Lot B** — ajout direct `CreateMethodUser + JustAdd` (form paramTypes). Sous-ensemble = 22 classes (ABB B2x, ABB Terra AC RTU/TCP, génériques/simulés nymea). ⚠️ **Les Eastron SDM N'EN SONT PAS** (ils sont Discovery → lot C). Piège param : nom du master varie (`modbusMasterUuid` vs `rtuMaster`). Reporté **après** C. - **Lot D** — discovery → pairing (OAuth/PushButton/UserAndPassword). - **Connexions Étape 3** — découverte **mDNS** native (`multicast_dns`, natif only ; web = ajout manuel). Différée. - **Connexions — branche CreateUser** (box neuve) : codée mais **non testée live** (pas de box vierge/factory-reset sous la main). - **Energy Phase 2/3/4** (session dédiée, voir mémoire) : Phase 2 = states ratios côté energymanager (autre repo) ; Phase 3 = persistance Influx ; Phase 4 = refonte flow-card §1.1 + bandes + retrait définitif du calcul app-side. Contrat = `docs/UI_data_contract.md`. ### Bugs latents notés (hors lot) - `dashboard_screen.dart` : `onRefresh: () => service.startSimulation()` → sur box réelle, pull-to-refresh bascule en démo. À corriger au câblage du refresh réel. - Affichage signe : sans `.abs()`, conso reste net-signée → Maison (flow-card) & ligne conso (graphe) montrent le signe brut **jusqu'à la Phase 4** (voulu). --- ## 5. Banc de test | Cible | Détail | |---|---| | **hems** `.75` | nymea 1.15.2, **ouverte** (`authenticationRequired:false`). Master RTU `/dev/ttyUSB0` présent mais bus **hors-ligne**. Sert aux sondes read-only sans auth. | | **box 2** `192.168.1.120` | **auth requise** (`initialSetupRequired:false` → login). Vraie installation avec **SDM120 PV**. | | **InfluxDB** | local sur `etm-powersync-dev`, **v1.6.7** (PAS Influx 3 Core), DB `nymeatest`, RP tiers déjà présentes. | | **Tél Android** | Redmi Note 9S, **adb WiFi** `192.168.1.107:5555`. | **PIN installateur par défaut = `1234`.** ### Comment lancer / tester - **Réel (réseau)** : `flutter run -d 192.168.1.107:5555` (Android WiFi). **Pas Chrome** pour le réseau (CORS LAN). Voir mémoire `web-lan-cors`. - **Linux natif** : ❌ bloqué (Flutter **snap** sans linker `ld.lld`). Web headless = page blanche (CanvasKit). Captures fiables = `adb exec-out screencap -p`. - **Sonde RPC sans GUI** : script `dart:io` `WebSocket.connect('ws://:4444')` + JSON-RPC (ex. utilisé pour valider Modbus/discovery contre `.75`). --- ## 6. Faits techniques à ne pas reperdre - **Signes nymea** (power balance agrégé) : `production` **positif**, `consumption` **NÉGATIF**, `acquisition` **net-signé** (négatif = export). `currentPower` thing-level = **négatif pour un producteur** (convention conso). **Ne jamais deviner/`.abs()` le signe.** - **Cumuls** (`totalAcquisition` / `totalReturn`) **séparés** → autoconso/autonomie dérivables des logs. UUID Hello **brace-wrapped** `{...}` (normalisé dans `_finishConnect`). - **Discovery SDM** : discoveryParam = **`slaveAddress` uniquement** (Int, déf 1). Le master n'est **PAS** une entrée — il **ressort** dans `descriptor.params`. Ajout final = `AddThing(thingDescriptorId)`. - **CreateMethod** = {User, Auto, Discovery} · **SetupMethod** = {JustAdd, DisplayPin, EnterPin, PushButton, UserAndPassword, OAuth}. Orthogonaux (une classe JustAdd peut exiger Discovery). --- ## 7. Reprise — prochaines actions probables 1. Brancher un **SDM sur `.75`** → sonde discovery réelle → lever les 3 points + **débloquer le merge du lot C**. 2. Puis **lot B** (ABB + génériques) ou **lot D** (pairing). 3. Côté energy : ouvrir la **session dédiée Phase 4** (contrat `docs/UI_data_contract.md`). 4. Décider du **push** de `feature/beta-add-config`. > Mémoires persistantes liées : `web-lan-cors`, `energy-chart-redesign`, `deploy`.