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>
This commit is contained in:
parent
c638ec6c52
commit
b5abe42fcc
358
docs/UI_data_contract.md
Normal file
358
docs/UI_data_contract.md
Normal file
@ -0,0 +1,358 @@
|
||||
# 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.
|
||||
Loading…
x
Reference in New Issue
Block a user