etm-powersync-app/docs/UI_data_contract.md
Patrick Schurig b5abe42fcc docs: contrat de données UI (rev.4) — qui calcule quoi
Source de vérité data-ownership UI, compagnon de DASHBOARD_SPEC.md.
Décisions figées rev.4 : flow §1.1 (Héos centre = métaphore, sens/couleurs),
ratios non calculés côté app (§2 → state energymanager), Things hand-off +
ModbusRTU d'abord, série canonique Héos Route B (§7.3), persistance Influx
(brut stocké + ratios dérivés + plan tagué run_id, §8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:07:16 +02:00

359 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ETM-PowerSync — Contrat de données UI (qui calcule quoi)
> Compagnon de `DASHBOARD_SPEC.md`. Tranche la question récurrente
> « **qui calcule les valeurs affichées ?** » pour chaque écran.
> Rev. 4 — §8.3 corrigé : **réutiliser l'InfluxDB nymea** (pas SQLite), DB ETM
> dédié + rollups natifs (retention policies) + checkpoint local pour le contrôle
> vivant. Alerte : tag Docker `latest` → InfluxDB 3 Core (72 h de rétention).
> Rev. 3 — ajout §7 (Héos data inputs & metering map) + §8 (persistance &
> historique : mesures brutes stockées / ratios dérivés / plan stocké).
> Rev. 2 — §2 corrigé après sonde terrain sur `.75` (nymead 1.15.2) :
> `GetPowerBalanceLogs` n'expose **aucun ratio**, seulement
> production/consumption/acquisition/storage + cumuls. La source canonique
> des ratios est donc le **state du plugin energymanager**, pas l'app,
> pas l'Energy experience nymea.
---
## 0. Principe directeur
**L'app Flutter est un moteur de rendu, pas un moteur de calcul.**
Source de vérité unique, comme `INTERFACE_etmvariableload.md` côté plugins.
Si l'app refait les maths de son côté, elle finit par **contredire l'arbitre**
(cf. Invariant 3 — charges pilotées au Grid mais hors surplus d'arbitrage ;
cf. phantom EV reservation — commandé vs mesuré). Un seul endroit calcule.
### Frontière
| Catégorie | Où ça se calcule | Raison |
|---|---|---|
| Sens / couleur des flèches du flow | **App** (dérivé du signe d'un état) | trivial, non ambigu, instantané |
| Surplus d'arbitrage | **energymanager plugin** | respecte l'Invariant 3 |
| Auto-conso / autonomie / totaux jour | **energymanager plugin (state)** | nymea 1.15.2 ne logge pas les ratios (sonde `.75`) |
| Décisions / raisons en cours | **energymanager** (`loadActions[]`) | l'arbitre est seul à savoir *pourquoi* |
| Plan de la journée (futur) | **Héos** (etm-powersync-eos) | seul à avoir forecast + MILP |
| Plan de la journée (passé) | **nymea energy logs** | historique réalisé |
### Couches (rappel)
1. **nymea things** → états instantanés bruts (P par compteur) + energy logs (historique).
2. **energymanager plugin** → surplus (Invariant 3), décisions+raisons, ratios live.
3. **etm-powersync-eos (Héos)** → forecast + plan + raisonnement tarifaire.
4. **App Flutter** → rend 1+2+3. Ne dérive QUE le sens des flèches.
---
## 1. Flux énergétique (flow-card)
### 1.1 Direction & couleur — règles figées
| Nœud | Comportement | Sens des particules | Couleur |
|---|---|---|---|
| **Solaire (PV)** | producteur pur | toujours PV → hub | jaune `#FEC113`, jamais inversé |
| **Maison** | consommateur pur | toujours hub → Maison | bleu `#31A3DD`, jamais inversé |
| **Réseau** | bidirectionnel | import = Réseau→hub / export = hub→Réseau | import **rouge** (coût) · export **vert atténué** (revente faible) |
| **Batterie** | bidirectionnel | charge = hub→Bat / décharge = Bat→hub | décharge **vert** (aide) · charge **neutre/bleu** |
> La convention de signe doit être **explicite et documentée** (un seul endroit).
> Sur la capture actuelle : Réseau en rouge vers le centre = import 2.4 kW ✓.
> Vérifier que le sens batterie reflète bien charge vs décharge.
### 1.2 Héos au centre — décision à prendre
Physiquement **faux** : Héos est le contrôleur, pas un nœud électrique.
Le vrai point de couplage AC est au centre, Héos est à côté.
- **Option A** — garder Héos au centre comme « le cerveau ». Les lignes = la
conscience/le contrôle, pas le câble. Métaphore assumée. Fort en branding.
Acceptable car la carte parle *de pilotage*.
- **Option B** — nœud central = point de couplage (busbar AC neutre), Héos en
badge/overlay. Physiquement correct, plus sobre.
**Reco** : A est défendable tant que l'animation respecte 1.1. Ne pas laisser
les particules « traverser » Héos dans un sens physiquement impossible.
### 1.3 Mono vs multi-onduleur
- Le **nœud Solaire du diagramme affiche TOUJOURS l'agrégat** (somme). Ne jamais
multiplier les nœuds dans le schéma — ça le casse visuellement.
- Le détail (tap sur `+`) gère la multiplicité :
- 1 onduleur → prod + strings si dispo.
- N onduleurs → agrégat en tête + liste par onduleur.
### 1.4 Détails Maison & compteur non mesuré (ex. PAC)
- Nœud Maison = consommation **totale**, dérivée :
`Maison = PV + import_grid + décharge_bat export_grid charge_bat`.
- Le détail ne montre des sous-postes **que pour les compteurs qui existent**.
- **Charge connue mais non mesurée (PAC sans compteur dédié)** :
- ne **jamais** afficher un faux 0 ni une estimation inventée (anti-oversell) ;
- soit on l'omet, soit on affiche « non mesuré » explicitement.
- Décision implicite à acter : la décomposition par appareil **nécessite un
compteur par appareil**. Pas de compteur PAC = pas de ligne PAC.
---
## 2. Cartes Auto-conso / Autonomie
- **Agrégats sur une période** → **jamais** calculés par l'app (sinon dérive
selon le rythme de polling, et désaccord avec le bilan/logs).
- **Réalité 1.15.2 (sonde `.75`)** : `GetPowerBalanceLogs` n'expose **aucun
ratio ni décomposition par bande** — seulement production / consumption /
acquisition / storage + **cumuls**. La prémisse « ratios déjà calculés
backend » est donc fausse littéralement.
- **Source canonique = state du plugin energymanager** (une seule source).
Pas l'app, pas l'Energy experience. Justification : ce n'est pas un pis-aller,
c'est le **seul composant capable de calculer juste** (voir ci-dessous).
### 2.1 Formules déterministes (depuis cumuls, zéro drift de polling)
```
autonomie = (consommation import) / consommation
autoconsommation = (production export) / production
```
- **autonomie** : dérivable directement, `import` est dans les logs. ✅
- **autoconsommation** : exige **l'export (return) comme cumul DISTINCT**.
⚠️ **Point bloquant à vérifier sur la sonde** : `acquisition` est-il *net
signé* (import export en un nombre) ou import/export **séparés** ?
- séparés → dérivable depuis logs ;
- net → **non récupérable** des logs ⇒ l'energymanager doit **intégrer
import et export lui-même** (il voit la P réseau signée en continu, +
connaissance Invariant 3). C'est ce qui rend #2 *architecturalement obligé*.
- Ces formules sont **correctes avec batterie** : import/export au compteur
réseau nettent déjà les flux batterie ; PV→batterie→maison compte comme
autoconsommé (pas exporté). Pas de correction round-trip.
### 2.2 Bande autoconso instantanée (graphe Puissances)
- Même propriétaire : `selfConsumptionPower = max(production max(export,0), 0)`.
- À exposer en **state plugin**, pas dérivé app — sinon re-bug de signe à chaque
refonte (le `.abs()` interim n'était qu'un pansement sur un calcul app-side).
---
## 3. Carte décisions — AVEC vs SANS Héos (frontière open-core)
La carte actuelle parle **au futur** : « coût réseau ce soir 0,22 € »,
« objectif 80 % pour 18h ». Ça suppose un **forecast + un plan** = Héos.
### 3.1 Avec Héos (tier Predict/AI)
Inchangé : raisonnement tarifaire, objectifs horaires, plan. Texte = sortie Héos.
### 3.2 Sans Héos (tier Community, rule-based)
Le rule-based **n'a ni forecast ni plan**. Il ne peut donc pas tenir ce discours.
- En-tête : « **Pilotage automatique** » (pas « Héos pilote »).
- Décisions exprimées **au présent, réactives** :
- « Surplus solaire 2.0 kW → chauffe-eau activé »
- « Pas de surplus → voiture en pause »
- **Zéro promesse au futur** (pas de « ce soir », pas de « objectif 18h »).
Honnêteté : ne pas faire croire à une intelligence prédictive absente.
- Source du texte : `loadActions[]` de l'energymanager (la *raison* additive
qu'on a déjà figée), pas une reconstruction côté app.
### 3.3 Conséquence sur « Plan de la journée »
Voir §4 : sans Héos, la partie future (hachurée) disparaît. Surface d'upsell
naturelle : « Activez Héos pour planifier la journée ».
---
## 4. Plan de la journée
- **Passé (barres pleines)** = énergie réalisée → **nymea energy logs**.
- **Futur (barres hachurées)** = **Héos** (etm-powersync-eos → socket →
energymanager → state/endpoint exposé). L'app ne projette **rien** elle-même.
- Sans Héos : n'afficher **que le passé**. Pas de projection bricolée côté app.
- Légende (Maison / ECS / VE / Batterie / Réseau / Batt→maison) = catégories
de l'allocation Héos ; mêmes clés que le plan, pas de remapping côté app.
---
## 5. Config — Things (placeholder actuel)
L'app n'est **pas** un fork de nymea-app → il faut réimplémenter les RPC
(`Integrations.*` : GetThings, GetThingClasses, GetVendors, DiscoverThings,
AddThing/PairThing/ConfirmPairing, EditThing, RemoveThing). C'est gros, et
nymea-app le fait déjà bien. **Décision de périmètre à acter explicitement** :
- **Option MVP** — liste read-only des things + actions simples (renommer,
activer/désactiver, supprimer). 80 % de la complexité du wizard est dans
l'ajout avec discovery/pairing/paramètres — on l'évite.
- **Option ciblée** — réimplémenter le wizard **seulement pour TES classes**
(eastron, abbterra, v2c… dont tu maîtrises les params).
- **Option hand-off** — la config avancée reste dans nymea-app (installateur),
l'app ETM se concentre sur l'experience énergie (client final).
→ Question vraie : le client final a-t-il **besoin** d'ajouter des things, ou
c'est un acte installateur (toi/Mathieu) ? Si installateur-only, ne pas
construire un demi-wizard.
---
## 6. Config — Protocoles → ModbusRTU master
Surface **finie et bien définie**, dont **tes propres plugins compteurs
dépendent** (SDM, abbterra) → vaut le coup en natif.
- `ModbusRtu.GetSerialPorts` → lister les ports.
- `GetModbusRtuMasters` / `AddModbusRtuMaster` / `Reconfigure…` / `Remove…`.
- Params : serialPort, baudrate, parity, stopBits, dataBits.
→ Petit, fermé, et bloquant pour tes meters. Le construire avant le wizard Things.
---
## 7. Héos — data inputs & metering map
### 7.1 Règle de granularité
**nymea ne stocke que ce qui est compté.** Un consommateur n'a de série de
puissance que s'il est un *thing avec un state `currentPower`* (interfaces
`smartmeter`/`energymeter`/consumer/producer, `logged: true`).
**La granularité d'optimisation Héos = la granularité de comptage.**
⚠️ **Convention de signe nymea** : `currentPower` est toujours du point de vue
**consommateur****négatif pour un producteur** (le PV a un `currentPower` < 0).
Signe canonique, à ne jamais « deviner » côté app (cause du bug autoconso / `.abs()`).
### 7.2 Carte de comptage (état du parc)
| Consommateur | Thing ? | `currentPower` ? | Héos optimise finement ? |
|---|---|---|---|
| Réseau (GRID) | | | référence (root meter) |
| PV / onduleur | | (négatif) | oui, **par onduleur** |
| Batterie | | | oui |
| VE (wallbox) | | | oui |
| ECS (relais SDM) | | ou déduit du niveau | oui |
| **PAC** | SG-Ready | `sgReadyMode` seul | **non — pilotable, pas mesurable** |
| **Clim (Zenkeo/Tuya)** | bridge | seulement si DP Tuya puissance | partiel |
| Maison (base) | dérivé | résidu = total charges pilotées | `Maison_base` |
- **Trou PAC** : SG-Ready n'expose que `sgReadyMode` (Off/Low/Standard/High),
aucune puissance. Sans compteur dédié, Héos **pilote** la PAC mais ne la
**mesure** pas ; sa conso reste noyée dans `Maison_base`.
- **Clim** : seulement si `clim_bridge.py` remonte un state puissance.
- **Production par panneau de toit : non.** Par onduleur oui ; par string
seulement si le plugin expose les states MPPT. Ne pas concevoir Héos en
supposant une granularité panneau.
- **Conséquence** : toute optim PAC/clim fine est une décision **matérielle**
(poser un compteur / exposer un DP), pas logicielle.
### 7.3 Alimentation de Héos — Route B (tranché)
- **States = source live**, lus en in-process par le plugin.
- **energymanager = série historique canonique** par consommateur/générateur,
échantillonnée à la résolution du MPC (buckets 15 min), Invariant-3-correcte.
- **logs nymea = filet / contrôle croisé**, pas l'entrée primaire de Héos.
- Justification : la Route A (logs nymea) impose le downsampling/rétention de
nymea sans contrôle du schéma, et ne sépare pas import/export. Seul le plugin
peut scinder import/export depuis la P réseau signée et figer la décomposition.
---
## 8. Persistance & historique
> Deux classes de données aux cycles de vie **opposés**. C'est la règle centrale.
### 8.1 Mesures — stocker le brut, dériver les ratios
- On stocke le **brut intégrable** par bucket : production, consommation,
import, export (séparés !), + par charge pilotée, + charge/décharge batterie.
- Les **ratios ne sont JAMAIS stockés** comme donnée primaire vues dérivées :
```
autonomie = (Σ conso Σ import) / Σ conso
autoconsommation = (Σ prod Σ export) / Σ prod
```
recalculables à la lecture pour **n'importe quelle période** (jour / semaine
/ mois / plage custom). Stocker des ratios figés = verrouillage de granularité.
### 8.2 Plan — stocker explicitement (non recalculable)
- Un **ratio se reconstruit à l'infini** depuis le brut.
Un **plan ne se recalcule jamais après coup** : il dépendait des prévisions
PV/conso/tarif **à T0** et de la solution MILP de cet instant.
**S'il n'est pas persisté au moment où il est produit, il est perdu.**
- Schéma : `(produced_at, bucket_start, asset, planned_value [, inputs_hash])`.
stockable dans Influx tagué par `plan_run_id` ; le plan **en cours
d'exécution** est en plus checkpointé localement 8.3).
- Cadence limitée : ne pas stocker chaque tick MPC. Garder un **plan-of-record**
(ex. 1/h ou snapshot début+fin de journée), pas chaque itération.
- Débloque **plan vs réalisé** : overlay prévu/réalisé confiance client +
matière première du federated-learning-lite (erreur de prévision).
### 8.3 Store & rétention (réalité embarquée)
**Réutiliser l'InfluxDB déjà présente** (nymea s'en sert). L'argument « service en
plus » ne tient pas : le coût est déjà payé. Influx est faite pour les séries ;
ses **retention policies + downsampling (CQ 1.x / tasks 2.x)** font les rollups
**nativement** moins de code qu'un SQLite hand-rollé. Et configurer la rétention
**corrige au passage le « shard accumulation »** qui faisait crash-looper l'instance
(les shards s'empilaient faute de purge).
- **DB / bucket ETM dédié** pas dans les measurements nymea. Isole ta donnée des
upgrades/migrations nymea et te donne ta propre rétention.
- **Rollups par paliers** (retention policies dédiées) :
- 15 min ~30 jours · horaire ~2 ans · journalier indéfini
- **Pinner la version Influx.** Le tag Docker `latest` pointe désormais vers
**InfluxDB 3 Core**, qui en OSS impose **72 h de rétention** + limite 5 DB
destruction silencieuse de l'historique. Le diagnostic « shard accumulation »
= TSM = on est en **1.x / 2.x** (bonne version). Ne jamais laisser un upgrade
auto basculer en 3 Core.
- **Checkpoint local minuscule** (fichier / petit SQLite) pour l'**état de contrôle
vivant** (plan en cours d'exécution, dernier setpoint). Si Influx tombe, Héos ne
doit **ni hard-fail ni perdre le plan exécuté** dégradé propre, cohérent avec
le L2 watchdog. ***Logging store down arbitrage down.***
- Distinct du log engine nymea (filet brut générique) : la série dans le DB ETM est
la **série curée** (Invariant-3, import/export scindés) dont dérivent Héos *et*
l'historique client.
### 8.4 Read model de l'historique client
```
période choisie
→ agrège les mesures au palier de rollup correspondant
→ dérive autoconso / autonomie (§8.1)
→ overlay plan-of-record (§8.2) si dispo → vue « prévu vs réalisé »
```
Même donnée parcourue dans les deux sens du temps : passé = mesures réalisées,
futur = plan Héos. La metering map §7.2 est le socle commun (entrée Héos + bandes
du « Plan de la journée »).
---
## Récap des décisions
1. Héos au centre du flow : **Option A — métaphore « cerveau » assumée**
(condition : animation respecte les sens figés §1.1, jamais de particule à
contre-sens à travers Héos). tranché.
2. Source des ratios : **state du plugin energymanager** (ni app, ni Energy
experience). Court terme : dérivation depuis cumuls via §2.1, **sous réserve**
que l'export soit loggé séparément sinon le plugin intègre import/export
lui-même. tranché.
3. Périmètre Things app : **hand-off nymea-app + MVP read-only client** ;
**ModbusRTU master en premier** 6, bloquant pour SDM/abbterra). Wizard
ciblé seulement si vraiment nécessaire. tranché.
4. Carte sans Héos : wording **présent/réactif** + masquage du futur, texte
issu de `loadActions[]`. tranché.
5. Alimentation Héos : **Route B** states (live) + série canonique
energymanager (15 min, Invariant-3), logs nymea en filet. tranché.
6. Persistance : **mesures brutes stockées + ratios dérivés** ; **plan stocké
explicitement** (non recalculable) ; **réutiliser l'InfluxDB existante** (DB ETM
dédié, rollups via retention policies) + **checkpoint local** pour l'état de
contrôle vivant. pinner Influx en **1.x/2.x** (pas 3 Core / 72 h). tranché.
### Reste ouvert (à lever avant code §2 / §8)
- `acquisition` dans `GetPowerBalanceLogs` = **net signé** ou **import/export
séparés** ? Détermine si l'autoconso est dérivable des logs ou doit être
intégrée par le plugin 2.1 / §8.1).
- **Carte de comptage §7.2** : décider quels consommateurs non mesurés (PAC,
clim) justifient un compteur matériel pour une optim fine.