Le fil conducteur des six commits du jour tient en une phrase : l'app ne recalcule pas ce que la box publie, et n'anticipe pas ce qu'elle n'a pas encore publié. Le point à retenir est le seuil de réserve batterie : 0,9 en usine, batterie du banc à 50 %. Quand la règle montera dans le budget, `.75` ne pilotera plus rien par grand soleil, sans erreur ni panne. Le RECAP le dit, et le TODO garde les trois crochets laissés ouverts — dont le `Set` volontairement vide qui attend le code du moteur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
658 lines
43 KiB
Markdown
658 lines
43 KiB
Markdown
# RECAP — etm-powersync-app · reprise de session
|
||
|
||
> **Session 2026-08-26.** Branche **`feature/beta-add-config`**, **poussée** (`801ba15`).
|
||
> La journée du **26** est en tête ci-dessous ; la soirée du **25** (lot C) suit ; la section
|
||
> « État au 2026-06-30 » plus bas reste valable pour tout ce qu'elle décrit.
|
||
|
||
---
|
||
|
||
## Journée 2026-08-26 — se recaler sur la box, trois fois de suite
|
||
|
||
La box a bougé trois fois dans la journée (`+etm16`, `+etm19`, puis `3g`). Le fil conducteur
|
||
de tout ce qui suit tient en une phrase : **l'app ne recalcule pas ce que la box publie, et
|
||
n'anticipe pas ce qu'elle n'a pas encore publié.**
|
||
|
||
| Commit | Contenu |
|
||
|---|---|
|
||
| `aada8e2` | Les 4 points ouverts du RECAP : glissement, sélecteur de mécanisme, chip de domaine, thème |
|
||
| `7089148` | Le harnais vérifie lui-même le chip et l'écran de mécanisme |
|
||
| `cd76df3` | **`+etm16`** — table de paliers, `stagesW`, compteur et sonde |
|
||
| `ab2a27b` | **`+etm19`** — `BELOW_MIN_POWER`, compteurs rattachés depuis l'écran, glissement exercé |
|
||
| `449c6ac` | **`3g`** — les valeurs de borne sont du RUNTIME |
|
||
| `801ba15` | **`3g`** — la réserve batterie sort de l'usine |
|
||
|
||
### Ce qui a été corrigé, et pourquoi ça avait échoué
|
||
|
||
- **Le glissement ne marchait pas** — et c'était ma régression : j'avais imbriqué un
|
||
`ReorderableListView` dans un autre, les deux se disputaient le geste. Le classement est
|
||
désormais **inversé à dessein** : on glisse les *domaines*, et les flèches ordonnent *dans*
|
||
un domaine. Restait une **zone morte de 3 px** dans la poignée : `ReorderableDragStartListener`
|
||
délègue le test de toucher à ses enfants, un `Container` transparent la referme.
|
||
- **Le test du glissement déclarait forfait** deux fois sur trois : le banc revient
|
||
régulièrement à une charge par domaine, et sans domaine partagé il n'y a rien à glisser. Le
|
||
test **pose maintenant sa propre condition** — il regroupe, exerce, puis rend le classement
|
||
d'origine. Un test qui dépend de l'état du terrain ne teste rien.
|
||
- **« la config du rootmeter est perdue »** — elle ne l'était pas : la box l'avait toujours,
|
||
l'app ne la **lisait** jamais (`syncRootMeter()`). Le symptôme et la cause étaient à deux
|
||
écrans de distance.
|
||
- **`/energy/setup` est gardé par le mode installateur** : le routeur renvoyait ailleurs, et le
|
||
symptôme affiché était « aucune charge lue ». Le harnais déverrouille au PIN `1234` et
|
||
**reverrouille après** le maintien d'écran — l'ordre inverse passait au vert en montrant le
|
||
tableau de bord.
|
||
|
||
### Les trois recalages de contrat
|
||
|
||
**`+etm16` — ce que la box compare vraiment.** `sameHardware()` est public, et la comparaison
|
||
porte sur la **table de paliers**, pas sur la liste de relais : réordonner `relays[]` ne
|
||
reconstruit plus rien (vérifié au banc — « 2 inchangée(s) », aucun état sûr). Mon premier
|
||
miroir comparait des listes et criait au loup ; corrigé après lecture de
|
||
`RelayStageTable::operator==` — chaque combinaison se compare **comme un ensemble**, mais les
|
||
paliers s'apparient par position. Les paliers dérivés arrivent maintenant en `stagesW` : un
|
||
miroir de moins à faire diverger. Les trois champs optionnels (`meterThingId`, `sensorThingId`,
|
||
plancher) sont traités **comme un seul objet**, grisage dérivé du schéma, jamais anticipés dans
|
||
le modèle.
|
||
|
||
**`+etm19` — `BELOW_MIN_POWER`.** Deux variantes, à distinguer de `SURPLUS_INSUFFICIENT` et
|
||
`SG_NORMAL` qui ne se déclenchent plus que sur budget ≤ 0. Le repli aurait tenu : un code
|
||
inconnu n'a jamais affiché de vide.
|
||
|
||
**`3g` — deux valeurs de runtime, et un réglage qui change de portée.** « Minimum » désigne
|
||
deux objets différents : le **plancher déclaré** d'une charge modulable (une propriété de
|
||
l'appareil, qui se règle) et le **minimum d'une borne**, `max(borne, voiture)`, qui dépend du
|
||
véhicule branché et du nombre de phases — de l'ordre de 1,4 kW en monophasé contre 4,1 kW en
|
||
triphasé, **pour le même courant**. L'app affiche la valeur telle que la box la donne : aucune
|
||
conversion côté app, sinon il faudrait deviner laquelle des deux règles s'applique. Et le
|
||
budget **ne se partage pas** entre bornes : aujourd'hui chacune reçoit l'enveloppe entière.
|
||
|
||
### La réserve batterie — le point à retenir de la journée
|
||
|
||
`batteryLevelConsideration` vaut **0,9** en usine. Le banc `.75` a sa batterie à **50 %** : le
|
||
seuil est au-dessus de ce que la batterie atteint. Aujourd'hui ça ne gèle que la recharge du
|
||
véhicule ; **quand la règle montera dans le budget, `.75` ne pilotera plus rien** — ni ECS, ni
|
||
PAC, ni bornes — par grand soleil, sans erreur ni panne, rien à lire dans un journal.
|
||
|
||
Un paramètre qui peut produire ce symptôme n'est pas un paramètre d'usine.
|
||
`BatteryReserveCard` le lit, l'écrit, **alerte quand le seuil dépasse le SOC réellement
|
||
atteint**, et dit la portée à venir : régler ce seuil « pour la voiture » couperait le reste
|
||
demain. Elle n'invente aucun défaut — box muette, carte muette.
|
||
|
||
Le motif d'arbitrage est **préparé, pas anticipé** : la clé ARB `decisionBatteryReserve` existe
|
||
et `kCodesReserveBatterie` est un `Set` **volontairement vide** dans `telemetry_text.dart`. Le
|
||
crochet attend le code que publiera le moteur ; deviner son nom aurait produit un affichage qui
|
||
ne se déclenche jamais.
|
||
|
||
### Vérifié sur l'appareil, contre `.75`
|
||
|
||
Campagne verte, zéro exception. La notification précède l'accusé (4/4 écritures).
|
||
`enabled:false` ouvre bien les contacts (ECS-413). Les deux compteurs **PAC-Meter** et
|
||
**ECS-Meter** sont rattachés **depuis l'écran**, et `measuredW` est désormais **distinct** par
|
||
charge — chauffe-eau 1 500 W, PAC 800 W, cette dernière affichant « Mesuré 800 W · sous la
|
||
consigne » face à son estimation de 3 000 W.
|
||
|
||
> Deux pièges de méthode, notés parce qu'ils se représenteront :
|
||
> j'ai d'abord **affirmé des `measuredW` distincts** pour prouver le rattachement — deux
|
||
> compteurs au repos lisent légitimement 0,0 W tous les deux ; l'assertion porte maintenant sur
|
||
> des `meterThingId` distincts plus un détachement montrant `null` ≠ 0. Et une assertion
|
||
> cherchait les cartes **sans faire défiler** : dans un `ListView`, ce qui n'est pas à l'écran
|
||
> n'est pas construit — elle ne prouvait que la hauteur de l'écran.
|
||
|
||
### Reste ouvert
|
||
|
||
- `minPowerW == null` **sans véhicule branché** : pas exercé sur appareil, la borne n'entre pas
|
||
dans `loads[]` avant `3g`.
|
||
- `kCodesReserveBatterie` à câbler quand le moteur publiera le code du motif.
|
||
- **Les zones** (`ac_screen.dart` est encore de la donnée fictive, `.75` n'en déclare aucune) —
|
||
explicitement **à ne pas entreprendre**.
|
||
|
||
---
|
||
|
||
## Soirée 2026-08-25 — LOT C (ci-dessous)
|
||
|
||
---
|
||
|
||
## Soirée 2026-08-25 — LOT C : les écrans de configuration par charge
|
||
|
||
### Ce qui a été livré
|
||
|
||
| Commit | Contenu |
|
||
|---|---|
|
||
| `2a8ad95` | **Lot C** — fusion des deux écrans, écrans de mécanisme, grisage par introspection |
|
||
| `f0aa0fd` | Harnais `integration_test` — l'IU sur le téléphone, contre `.75` |
|
||
| `98e2874` | Diagnostic de montage d'écran (MIUI bloquait l'installation) |
|
||
| `15a5263` | **L'IU tourne sur l'appareil** : chaîne complète vérifiée contre `.75` |
|
||
| `38475e0` | Compteurs réseau honnêtes + le sélecteur clair/sombre s'applique partout |
|
||
| `3d2aab2` | **Les écrans suivent enfin la maquette** — sélecteurs, table d'états, budget, borne |
|
||
|
||
- **L'écran « Ordre de service » a disparu** ; son chemin de données (`LoadConfigProvider`) a
|
||
migré dans « Rôles & appareils », jamais l'inverse. L'entrée de menu est retirée.
|
||
Les charges viennent de `GetLoadConfig`, plus de `LoadDescriptor` reconstruits depuis des
|
||
`EmsRole` — LM-201 interdit de toute façon de déclarer `dhw` **et** `heatPump` pour une PAC
|
||
sans ballon séparé.
|
||
- **Trois écrans de mécanisme** (routeur de relais / modulable / SG-Ready) avec sélecteurs de
|
||
Things **par nom** filtrés sur l'interface nymea, table d'états SG-Ready éditable
|
||
(SG1/SG2 choisis une fois, en haut), temporisations, « Compteur dédié » grisé avec sa
|
||
raison, « Inclure dans l'arbitrage », et **validation avant envoi**.
|
||
- **Grisage dérivé de `JSONRPC.Introspect`**, jamais codé en dur : trois états et non deux —
|
||
écrivable, `r:` (mesure, pas fonction manquante), absent — plus « inconnu » quand le schéma
|
||
n'a pas pu être lu.
|
||
- **`setChargingInfo()` était INOPÉRANT** : namespace `EnergyPlugin.*` inexistant, champs
|
||
`mode`/`targetSoc`/`endTime`/`minCurrent` au lieu de
|
||
`chargingMode`/`targetPercentage`/`endDateTime`. Vérifié par appel réel :
|
||
`Missing required key: chargingMode`. Corrigé, et le refus remonte désormais au lieu d'être
|
||
avalé pendant que l'état local mentait.
|
||
- **Tableau de bord** : « Depuis réseau » lisait l'**opposé** du compteur d'export au lieu de
|
||
`totalAcquisition` — 0,0 kWh en permanence. Et les deux tuiles disaient « aujourd'hui » sur
|
||
des compteurs cumulés. Corrigé, avec un plafond de plausibilité contre les valeurs
|
||
corrompues que la box publie (cf. brief plugin §1).
|
||
- **Thème** : 126 sites basculés vers les variantes contextuelles ; 9 restent en constante
|
||
(tables statiques sans `BuildContext`).
|
||
|
||
### Vérifié SUR L'APPAREIL, contre `.75`
|
||
|
||
```
|
||
[appareil] schéma lu — nymea 1.15.2+… · NymeaEnergy 0.8 · AirConditioning 1.1
|
||
[appareil] écran de configuration monté
|
||
[appareil] charges lues : [chauffe-eau, pac-terrain]
|
||
[appareil] écriture → LoadSaveState.confirmed
|
||
[appareil] libellé restauré : chauffe-eau
|
||
```
|
||
|
||
`adb shell input` est refusé par MIUI ; la voie qui marche est `integration_test`, qui
|
||
s'exécute **dans le processus de l'app**. Détail et pièges : `docs/RELEVE_LOTC.md` §2.
|
||
|
||
### L'erreur de la soirée, et sa leçon
|
||
|
||
**Les écrans ont d'abord été construits sans ouvrir `docs/mockups/`.** Les maquettes — dont
|
||
celle des mécanismes, mise à jour le soir même — étaient dans le dépôt et décrivaient
|
||
précisément tout ce qui manquait ensuite. Tout a été à refaire (`3d2aab2`).
|
||
**`docs/mockups/*.html` fait autorité sur la forme des écrans**, au même titre que
|
||
`INTERFACE_etmvariableload.md` sur le contrat.
|
||
|
||
### Ouvert — repris le 2026-08-26 au matin
|
||
|
||
1. **Le glissement (drag & drop)** ne semble rien produire. À départager : le geste est-il
|
||
avalé par le `ListView` parent, ou n'y a-t-il simplement rien à déplacer ? Sur `.75` il n'y
|
||
a **qu'une charge par domaine** — le test décisif est de mettre les deux dans le même
|
||
domaine.
|
||
2. **Le sélecteur de mécanisme est non cliquable**, et c'était mon choix — à tort : la maquette
|
||
le donne comme un champ enregistré (`enregistré · adapter`). À rendre actif, avec la
|
||
bascule de charge utile et l'avertissement de reconstruction.
|
||
3. Le chip de domaine s'affiche **sans libellé** sur la carte de charge.
|
||
4. Les **9 sites de couleur** hors thème (tables statiques, peintres).
|
||
5. **Zones de climatisation** : `ac_screen.dart` n'appelle aucune méthode `AirConditioning`,
|
||
ses 4 pièces sont des constantes de maquette, et `.75` déclare zéro zone. Le câblage est un
|
||
lot en soi — la box, elle, expose **8 méthodes toutes en écriture**.
|
||
|
||
### Pour l'agent plugin
|
||
|
||
Sept constats côté moteur : `BRIEF_agent_plugin.md` dans le dépôt **plugin**. Le plus visible
|
||
est le §1 — les compteurs cumulés d'`Energy.GetPowerBalance` sont corrompus (9,1 × 10³³ kWh),
|
||
alors que le Thing compteur est sain.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
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
|
||
```
|
||
|
||
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 :
|
||
|
||
```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.
|
||
|
||
## 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)
|
||
|
||
1. **Aucune reconnexion automatique.** `_onDone()` pose `_connected = false` et 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.
|
||
2. **`/energy/setup` dé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 `.75` n'a **aucun** SolarEdge (11 things : Fronius ×3, Terra AC,
|
||
6 relais). La trace disait `SetRootMeter 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 bien `Fronius Three Phase Meter`. L'écran **affirme** dans les deux cas.
|
||
Cinq `[LoadConfig] N descripteur(s) construits mais NON émis` pendant l'exercice.
|
||
3. **Cumuls aberrants venus de la box.** `totalReturn = 9.10e+33 kWh`,
|
||
`totalAcquisition = 7.53e+33`, `totalConsumption = -1.57e+33` — seul `totalProduction`
|
||
est 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 de `EnergyRatiosInterim` ont tenu : ratios à `0 %` plutôt qu'une absurdité.
|
||
4. **Thème ignoré sur l'écran des charges.** Couleurs claires en dur (`Scaffold`
|
||
`0xFFF0F2F5`, AppBar blanche, textes `0xFF1A1A2E`) 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.md` dit pourtant
|
||
« utiliser `app_theme.dart` systématiquement ».
|
||
5. **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.
|
||
6. **Titre d'écran dupliqué** : la clé ARB `installerLoadsTitle` vaut « Rôles & appareils »,
|
||
soit le titre de l'AUTRE écran. Deux écrans, un seul titre.
|
||
7. **`load()` hors connexion efface l'instantané.** `getLoadTelemetry()` renvoie `null` pour
|
||
`!_connected` **et** 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.
|
||
8. **Bruit de transport.** Les notifications nymea portent un `id` : `_processMessage` les
|
||
journalise d'abord en « réponse non attendue (déjà timeout?) » avant de les traiter
|
||
correctement. Et le heartbeat interroge `Energy.GetPowerBalance` **toutes les 5 s** alors
|
||
que `Energy.PowerBalanceChanged` arrive 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 des `LoadDescriptor` reconstruits depuis des `EmsRole`. C'est
|
||
la cause racine du défaut 2 ; sans ça, un glisser-déposer n'y écrira jamais un `priority`
|
||
ré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 mode `fixed` d'`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 :
|
||
|
||
1. le namespace `LoadConfig` cherché dans `OPTIMIZER_PROTOCOL.md`, où il n'est pas (note de
|
||
périmètre ajoutée côté plugin, commit `afaa8c2`) ;
|
||
2. le « bug critique » `Energy.SetChargingMode` — **cette méthode n'existe pas**, l'appel
|
||
échouerait sur « No such method » ;
|
||
3. les champs de `ChargingInfo` (`mode`/`minCurrent`/`targetSoc`/`endTime` au lieu de
|
||
`chargingMode`/`targetPercentage`/`endDateTime`), le namespace `EnergyPlugin.*`
|
||
inexistant, et `AirConditioning` donné 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
|
||
|
||
1. **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.
|
||
2. **Capturer une fixture brute de défaut** au prochain passage de `fault_probe.dart`
|
||
(`--dump`).
|
||
3. **É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
|
||
|
||
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 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://<host>: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`.
|