Patrick a relancé le service à 18:23:45. LoadConfigStore recharge 2 configs depuis /var/lib/nymea/energy-load-configuration.json et la relecture RPC est identique clé par clé à celle d'avant l'arrêt : chauffe-eau→ecs, pac-terrain→hvac. Le redémarrage montre aussi la reprise à froid vue du dehors (ECS-411-b) : les adaptateurs journalisent ce qu'ils ont LU sur les contacts, avec la mention « correspondance exacte » — l'état de départ est constaté, pas supposé. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
20 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.
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.