flutter run n'imprime pas les traces dart:developer log() — elles partent au VM Service, ni sur stdout ni dans logcat. Ce pont est ce qui a rendu le passage sur appareil du 2026-08-25 exploitable ; il ne vivait que dans un scratchpad de session, alors que le RECAP le donnait « à récupérer au prochain passage ». Mode d'emploi en tête, avec le piège déjà payé une fois : le VM Service livre l'historique bufférisé d'un coup au streamListen, donc horodater à la réception écrase toute la chronologie sous une seule seconde. On lit event.timestamp. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
32 KiB
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:localedansHello,m_clientLocales). Pas de forçageLANG/LC_ALLdans l'unité systemd. Mais sur 784 chaînes affichables de.75, l'allemand en change 1, le français 0 : aucun catalogue.qmfr n'est livré. LesdisplayName= identifiants anglais. - Frontière RPC / runtime de l'arbitre : quasi rien ne sort. Seuls
degradedMode(notificationChargingSchedulesChangeduniquement — pas relisable parGet), les deux ratios d'énergie,chargingStatede l'EV et le plan de recharge franchissent. Allocation, budget, verrous, paliers,available, motifs : journal seulement.SurplusContext/LoadContextTelemetryn'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 parpatched()depuis la map d'origine, jamais de re-sérialisation.services/load_priority.dart—groupByDomain/flattenToPayload/applyDefaultDomainOrder/ miroir client devalidateSet().providers/load_config_provider.dart— séquencepending → 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. Extensioncontext.l10n. Français seul. NymeaEnergy.GetLoadConfig/SetLoadConfigdeviennent 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.
Reprise du 2026-08-25 (soir) — l'agent plugin a terminé, la moitié télémétrie est faite
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.
Livré côté app
| 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. |
57 tests (30 avant), flutter analyze 25 issues = ligne de base inchangée.
Trois choix de conception à ne pas défaire
- La fraîcheur ne repose pas sur le seul
timestamp. Trois signaux concourent : l'âge dutimestamp(le seul qui vaille à l'instant du premier instantané — unGetrend 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 letimestampa 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. - 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) ».
- 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
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.
La reprise à froid lit le matériel, elle ne le suppose pas. Au redémarrage de 18:23:45,
RelayRouter et SgReadyAdapter journalisent ce qu'ils ont lu sur les contacts —
« palier 0 W [correspondance exacte] ; relais lus : … », « état 3 (~1500 W estimés)
[correspondance exacte] ; contacts lus : … ». C'est ECS-411-b vu du dehors : l'état de
départ de l'arbitre est constaté, jamais reconstitué de mémoire.
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, redémarrage compris — é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, et survit au redémarrage de nymead (18:23:45, relancé par Patrick) : LoadConfigStore recharge 2 configs depuis le fichier et la relecture RPC est identique clé par clé à celle d'avant l'arrêt. |
| §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 :
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.
Passage sur appareil — 2026-08-25 (Redmi Note 9S, USB)
Premier passage de l'app sur matériel réel. adb voit l'appareil, mais MIUI refuse
l'injection d'événements (INJECT_EVENTS) : captures et journaux passent, input tap
non. Patrick a piloté à l'écran, l'agent a observé. Une capture par étape dans
screenshots/2026-08-25-passage-appareil/ (git-ignoré), trace complète dans son
trace-app.log.
Le pont qui a rendu la session possible
flutter run n'imprime pas les traces dart:developer log() — elles partent au VM
Service, ni sur stdout ni dans logcat. Tout ce que NymeaService._log() journalise était
donc invisible. Le pont tools/devlog/vm_logs.dart s'abonne au flux Logging du VM
Service et rend la trace. Sans lui, aucun des constats ci-dessous n'était atteignable.
Mode d'emploi en tête du fichier, piège d'horodatage compris (lire event.timestamp, pas
l'heure de réception : le VM Service livre l'historique bufférisé d'un coup).
Critères
| Critère | Verdict sur appareil |
|---|---|
| §7-9 locale | ✅ trois Hello (19:37:59, 19:43:55, 19:50:55), tous demandée "fr_FR" → retenue "fr_FR". La valeur relue vient de m_clientLocales côté box : c'est la box qui dit ce qu'elle a retenu, pas l'app qui se répond à elle-même. ⚠️ La locale de l'APPAREIL ne part pas : le téléphone est en en-GB, _requestedLocale est figé à 'fr_FR' (nymea_service.dart:61) et rien ne câble l'un sur l'autre. Conforme au brief (app française seule), mais à savoir le jour où l'app parlera allemand. |
| §7-5 télémétrie vivante | ✅ à l'écran, sans un geste : entre 19:51 et 19:52, budget 943 → 2 571 W, alloué 500 → 3 000 W, motif recomposé, « dernier cycle il y a 51 s » → « 41 s ». Le recrédit anti-clignotement est visible (0 → 500 W : la charge consommait déjà, on lui rend avant d'arrondir). |
| §7-6 fraîcheur | ⚠️ l'état « données anciennes » est INATTEIGNABLE par cette voie. Détail ci-dessous. |
| Écriture (§7-4 depuis l'UI) | ✅ affectation de domaine faite au doigt → « Configuration confirmée par la box » → pac-terrain → heating vérifié sur la box et dans le fichier persisté. |
| Acquittement ≠ application | ✅ mesuré : SetLoadConfig à 19:53:46.007, LoadConfigChanged à .288, acquittement traité à .303. La confirmation a battu l'ack de 15 ms. |
§7-6 — pourquoi la bascule n'a pas eu lieu
20:13:00 dernière trame LoadTelemetryChanged
20:13:20 coupure WiFi (adb shell svc wifi disable)
20:13:21 🔌 Connection closed
20:14:21 (T+60) → l'app est déjà sur « Installations »
20:17:47 (T+260) → toujours là. Seuil de 180 s jamais atteint à l'écran.
La garde de routage évacue l'écran avant que la fraîcheur ait le temps d'expirer.
hasActiveConnection tombe à faux, le routeur redirige vers /installations, et l'écran
qui devait virer au « données anciennes » n'existe plus.
Ce n'est pas un défaut de la fraîcheur — le compteur a été vu vivre et s'incrémenter à
l'écran (51 s → 41 s → 6 s). C'est que la perte de lien n'est pas le chemin qui mène à cet
état. Le seul déclencheur réel du bandeau est l'arbitre figé socket vivante : les
trames continuent d'arriver, le timestamp cesse d'avancer. C'est exactement le cas que le
battement de cœur du plugin existe pour rendre détectable — et il n'est pas provocable
depuis le réseau : il faudrait figer l'arbitre sans tuer nymead.
Conséquence de conception à retenir : des trois signaux de fraîcheur, seul
sinceCycleAdvanced peut réellement se déclencher dans l'app telle qu'elle est. Les deux
autres (cycleAge, sinceLastFrame) sont court-circuités par l'éjection.
Défauts constatés — aucun corrigé (constat d'abord, décision de Patrick)
- Aucune reconnexion automatique.
_onDone()pose_connected = falseet notifie, point final. Une coupure WiFi d'une seconde éjecte vers « Installations », fait retomber le mode installateur, et l'installateur doit tout refaire à la main. Observé deux fois. /energy/setupdécrit autre chose que l'installation. À 19:49 l'écran affichait « 6 / 6 rôles configurés · tout est assigné » avec un « Compteur réseau — Onduleur SolarEdge » — alors que.75n'a aucun SolarEdge (11 things : Fronius ×3, Terra AC, 6 relais). La trace disaitSetRootMeter ignoré (sim-inverter): des assignations héritées du mode démo avaient survécu dans la vue d'une box réelle. Et dans l'autre sens : après réassignation, l'app annonçait « Compteur réseau — aucun appareil » quand la box avait bienFronius Three Phase Meter. L'écran affirme dans les deux cas. Cinq[LoadConfig] N descripteur(s) construits mais NON émispendant l'exercice.- Cumuls aberrants venus de la box.
totalReturn = 9.10e+33 kWh,totalAcquisition = 7.53e+33,totalConsumption = -1.57e+33— seultotalProductionest sain. L'app les affiche fidèlement, sur trois lignes, en notation scientifique. Le défaut est en amont (compteurs cumulés de l'experience-plugin energy sur ce banc), mais une valeur physiquement impossible ne doit pas être mise en forme comme une donnée. Les gardes deEnergyRatiosInterimont tenu : ratios à0 %plutôt qu'une absurdité. - Thème ignoré sur l'écran des charges. Couleurs claires en dur (
Scaffold0xFFF0F2F5, AppBar blanche, textes0xFF1A1A2E) sur un téléphone en thème sombre : les titres de domaine et les libellés de charge passent en sombre-sur-sombre, presque illisibles. Reprises par mimétisme de l'écran existant —CLAUDE.mddit pourtant « utiliserapp_theme.dartsystématiquement ». - Trois débordements de texte : « Palier 3 000 W sur 3 50… », « Maintien ON — 60 s restantes (au dernier cyc… », et « Autoconsommat/ion » coupé en plein mot au dashboard.
- Titre d'écran dupliqué : la clé ARB
installerLoadsTitlevaut « Rôles & appareils », soit le titre de l'AUTRE écran. Deux écrans, un seul titre. load()hors connexion efface l'instantané.getLoadTelemetry()renvoienullpour!_connectedet pour « méthode absente » ; le provider traite les deux pareil et pose_telemetry = null→ l'écran n'affiche rien, en silence, au lieu de « données anciennes » ou de « box déconnectée ». À corriger avec la refonte : ce chemin sera emprunté chaque fois que l'app est hors ligne.- Bruit de transport. Les notifications nymea portent un
id:_processMessageles journalise d'abord en « réponse non attendue (déjà timeout?) » avant de les traiter correctement. Et le heartbeat interrogeEnergy.GetPowerBalancetoutes les 5 s alors queEnergy.PowerBalanceChangedarrive en poussé.
Décisions de Patrick prises pendant le passage
- L'ordre de service se règle dans « Rôles & appareils » → « Charges pilotables », au
glisser-déposer. L'écran « Ordre de service des charges » (
/energy/loads) disparaît, et son bloc télémétrie part sur la carte de chaque charge. - Le regroupement à deux niveaux est conservé (domaines ordonnés, charges ordonnées dedans).
- Le chip « À déclarer dans Configurer » devient un bouton Configurer, qui ouvre le détail du mécanisme : relay-router / etmvariableload / sg-ready, ses relais, ses Things.
- Conséquence assumée : le lot « label + enabled » et le lot « mécanisme + Things »
redeviennent un seul chantier — celui qui touche
sameHardware(), donc reconstruction des adaptateurs et réarmement des verrous à froid. - Cette refonte suppose que les cartes de « Charges pilotables » SOIENT les charges lues
par
GetLoadConfig, et non desLoadDescriptorreconstruits depuis desEmsRole. C'est la cause racine du défaut 2 ; sans ça, un glisser-déposer n'y écrira jamais unpriorityréel.
Refonte décidée le 2026-08-25 (soir) — maquettes validées
Patrick a tranché la question ouverte du RECAP : l'écran « Ordre de service des charges »
(/energy/loads) disparaît. Le rang se règle dans « Rôles & appareils → Charges
pilotables » au glisser-déposer, le regroupement à deux niveaux est conservé, et le bloc
de télémétrie descend sur la carte de chaque charge.
| Maquette | Contenu |
|---|---|
docs/mockups/charges_pilotables_mockup.html |
La liste : bandeau arbitrage, domaines glissables, cartes avec télémétrie, défaut, bouton Configurer |
docs/mockups/configurer_mecanismes_mockup.html |
Les six « Configurer » : relais · modulable · SG-Ready · borne · zones HVAC · sélecteur d'appareils |
docs/mockups/raccordement_reseau_mockup.html |
Compteur réseau + protection de surcharge |
Décisions de conception qui ne se déduisent pas des maquettes :
- Les paliers n'existent que chez le routeur de relais, et ils y sont déduits des
combinaisons de contacts, jamais saisis. Le modulable ne connaît qu'un plafond
(
maxPowerW). Le modefixedd'etmvariableload— que le plugin qualifie lui-même de legacy — n'est plus proposé, mais reste lisible : ouvrir l'écran ne doit pas convertir une config déployée. - Le critère de choix entre les deux ECS, formulé pour l'installateur : où sont les contacteurs ? Sur les sorties de la box → routeur de relais. Dans l'appareil, piloté par une consigne → modulable.
- SG1 / SG2 n'existent pas côté box.
sgReady.states[].relays[]n'est qu'une liste par état. Les étiquettes sont une convention d'app, retrouvée à la lecture par l'encodage standard (SG1 = fermé en état 1, SG2 = fermé en état 3). Un encodage non standard perd les noms, pas la configuration. - Le compteur par charge figure dans les cinq écrans, inactif, avec la raison sur place. Aucun champ n'existe côté box, et nymea rejette toute clé inconnue avant le handler.
- La borne règle un mode PAR DÉFAUT, pas une commande — le pilotage vit sur la carte véhicule du tableau de bord.
Ce qui remonte vers le plugin
Six points, rédigés en brief exploitable : docs/BRIEF_plugin_depuis_maquettes.md.
Le seul blocage dur est le premier — ChargingInfo n'a aucun champ de courant minimum,
donc le mode Min+PV reste sélectionnable mais non réglable.
Les cinq autres : motif de refus de SetLoadConfig qui ne franchit pas la frontière ·
WRITE_FAILED qui masque THING_MISSING · paliers atteignables non exposés (l'app devra
sinon dupliquer la combinatoire de relayrouter.cpp) · état matériel de enabled:false à
confirmer pour relais et modulable · estimatedPowerW du banc encore fictif.
Contrôle de la doc contre la machine — à refaire avant chaque lot
CLAUDE.md a été corrigé contre JSONRPC.Introspect le 2026-08-25. Trois fois dans la
journée la doc locale était en retard sur la box :
- le namespace
LoadConfigcherché dansOPTIMIZER_PROTOCOL.md, où il n'est pas (note de périmètre ajoutée côté plugin, commitafaa8c2) ; - le « bug critique »
Energy.SetChargingMode— cette méthode n'existe pas, l'appel échouerait sur « No such method » ; - les champs de
ChargingInfo(mode/minCurrent/targetSoc/endTimeau lieu dechargingMode/targetPercentage/endDateTime), le namespaceEnergyPlugin.*inexistant, etAirConditioningdonné pour 3 méthodes quand il en expose 8.
Toutes les méthodes citées par CLAUDE.md ont été revérifiées une par une : aucune
introuvable. La sonde tient en une ligne —
dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect.
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 | ✅ 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 domaine | ✅ sur .75, redémarrage de nymead compris |
| §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, 57/57 tests |
Ce qui reste à faire sur ce lot
- Faire tourner l'app sur appareil (
adbrefusé jusqu'ici). Tant que ça n'est pas fait, §7-6 et §7-9 restent des vérifications de protocole, pas d'écran. - Capturer une fixture brute de défaut au prochain passage de
fault_probe.dart(--dump). - Écran de réglage par charge — l'agent plugin le demande explicitement :
pac-terrainest 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
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.
RECAP — etm-powersync-app · reprise de session
État au 2026-06-30. Branche
feature/beta-add-config(9 commits au-dessus demaster). 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 viaflutter_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,.75ouvert, switch, déverrouillage installateur.
Rôles & appareils
EnergySetupProvidertri-état (present/absent/non-configuré), écranroles_devices_screen,LoadDescriptor(rév.2 : PAC exclue → SG-Ready),Energy.SetRootMeterréel, Get/SetLoadConfig en stub loggé.
Energy — Phase 1 (interim)
services/energy_ratios.dart= SEAM UNIQUEEnergyRatiosInterim: ratios autoconso/autonomie par Δ-de-cumuls (reseed jour/non-monotone, gardes n/a, clamp).selfConsumptionPowernet-signé sans.abs(). Test :test/energy_ratios_test.dart.- Calcul ratios app-side retiré de
_parsePowerBalance+ getterautoconsommationWsupprimé.
Installateur — Modbus RTU (validé live)
models/modbus_models.dart, 5 RPCModbusRtu.*dansnymea_service, écranprotocols_screen(CRUD), route/settings/system/protocols.
Installateur — Catalogue things lot A (validé live)
getVendors()+getAllThingClasses(), écranthing_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/ttyUSB0estconnected:false). 3 points marquésÀ REVALIDER SUR SDM RÉELdansthing_discovery_screen.dart/nymea_service.dart:- résolution master/slave par heuristique de nom de paramTypes (
'master'/'modbus'/'slave') ; - timeout scan (
_sendRequest= 15 s, à allonger si bus réel plus lent) ; title/descriptiondu descriptor (peuvent être vides/génériques).
- résolution master/slave par heuristique de nom de paramTypes (
- 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 (modbusMasterUuidvsrtuMaster). 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émoireweb-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:ioWebSocket.connect('ws://<host>:4444')+ JSON-RPC (ex. utilisé pour valider Modbus/discovery contre.75).
6. Faits techniques à ne pas reperdre
- Signes nymea (power balance agrégé) :
productionpositif,consumptionNÉGATIF,acquisitionnet-signé (négatif = export).currentPowerthing-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 =
slaveAddressuniquement (Int, déf 1). Le master n'est PAS une entrée — il ressort dansdescriptor.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
- Brancher un SDM sur
.75→ sonde discovery réelle → lever les 3 points + débloquer le merge du lot C. - Puis lot B (ABB + génériques) ou lot D (pairing).
- Côté energy : ouvrir la session dédiée Phase 4 (contrat
docs/UI_data_contract.md). - Décider du push de
feature/beta-add-config.
Mémoires persistantes liées :
web-lan-cors,energy-chart-redesign,deploy.