docs: contrat de données UI rev.5 — réserve décision-2 levée (cumuls séparés)

Intègre la vérif terrain (sonde .75) : GetPowerBalanceLogs sépare import/export
en cumulé (totalAcquisition / totalReturn) → autoconso/autonomie dérivables des
logs, la réserve de la décision-2 est levée. Aligne le doc tracké sur le code
(seam EnergyRatiosInterim, commit 3821bf3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-06-28 12:05:46 +02:00
parent 3821bf3fdb
commit f755c727ee

View File

@ -2,6 +2,10 @@
> Compagnon de `DASHBOARD_SPEC.md`. Tranche la question récurrente
> « **qui calcule les valeurs affichées ?** » pour chaque écran.
> Rev. 5 — sonde `.75` : réserve décision-2 **levée** (cumuls séparés) ; ajout de
> 3 garde-fous : 2 conventions de signe (§7.1), buckets par Δ-de-cumuls (§7.3),
> gestion reset compteurs (§2.1/§8.1) ; RP calquées sur le template nymea, Influx
> v1.6.7 confirmée.
> 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).
@ -113,14 +117,16 @@ 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é*.
- **autonomie** : `import` = Δ `totalAcquisition`. ✅
- **autoconsommation** : `export` = Δ `totalReturn`. ✅
**RÉSERVE LEVÉE (sonde `.75`)** : le réseau *instantané* est net-signé (un seul
champ `acquisition`), MAIS les **cumuls `totalAcquisition` / `totalReturn` sont
séparés**. Donc les deux ratios sont dérivables des logs par **deltas de cumuls**
— pas besoin que le plugin intègre import/export pour l'historique.
- ⚠️ **Compteurs = monotones requis.** Δ d'un cumul n'est valide que si le compteur
ne reset jamais. Sur reset (restart nymead, re-add thing, rollover) → Δ < 0 :
détecter (`Δ < 0` ⇒ reset, reseed sur la nouvelle valeur, ne pas créer un bucket
négatif). À gérer côté plugin **et** dans les CQ de rollup.
- 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.
@ -216,9 +222,19 @@ 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()`).
⚠️ **DEUX conventions de signe coexistent — ne pas les confondre** (piège « fix »
d'un faux bug) :
1. **Thing-level `currentPower`** : point de vue **consommateur** → **négatif pour
un producteur** (le PV a `currentPower` < 0). C'est la cause du bug autoconso /
`.abs()`.
2. **Power balance agrégé** (`production` / `consumption` / `acquisition`) :
`production` **positif**, `consumption` positif, `acquisition` **net signé**
(+ import / export). (Sonde `.75` : `acquisition = -231`, et un
`currentPowerProduction` **positif** au niveau balance — normal, ce n'est PAS
le `currentPower` thing-level.)
Le signe est **canonique des deux côtés** — l'app ne le devine jamais ; elle ne
dérive que le *sens* d'un flux à partir du signe (§1.1), rien d'autre.
### 7.2 Carte de comptage (état du parc)
@ -249,9 +265,16 @@ Signe canonique, à ne jamais « deviner » côté app (cause du bug autoconso /
- **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.
- **Construire les buckets par Δ de compteurs, pas par intégration du signal live**
(sonde `.75` : les cumuls sont séparés) :
- grid import/export du bucket = Δ `totalAcquisition` / Δ `totalReturn` ;
- conso par appareil = Δ `thing.totalEnergyConsumed` ;
- exact, pas de drift d'intégration. L'intégration du signal net-signé ne sert
qu'au temps réel sous le bucket (affichage), pas à la série stockée.
- ⚠️ même gestion de reset qu'en §2.1/§8.1 (Δ < 0 reset).
- Justification Route B : la Route A (logs nymea bruts) impose le downsampling/
rétention de nymea sans contrôle du schéma ; le plugin reste seul à figer la
décomposition Invariant-3 dans la série curée.
---
@ -263,6 +286,9 @@ Signe canonique, à ne jamais « deviner » côté app (cause du bug autoconso /
- On stocke le **brut intégrable** par bucket : production, consommation,
import, export (séparés !), + par charge pilotée, + charge/décharge batterie.
→ obtenu par **Δ de cumuls** (`totalProduction`, `totalReturn`,
`totalConsumption`, `totalAcquisition`, `thing.totalEnergyConsumed`), pas par
intégration du signal live (§7.3). **Gérer le reset**< 0 reseed).
- Les **ratios ne sont JAMAIS stockés** comme donnée primaire — vues dérivées :
```
@ -296,15 +322,18 @@ ses **retention policies + downsampling (CQ 1.x / tasks 2.x)** font les rollups
**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.
- **DB / bucket ETM dédié** — pas dans la base `nymeatest` de nymea. Isole ta
donnée des upgrades/migrations nymea et te donne ta propre rétention.
- **Calquer les paliers sur le template RP nymea existant** (sonde `.75`) plutôt
que d'inventer : `live 24h · minutes 7j · hours 3 ans · days 20 ans`
(+ `discrete 1 an`). Cohérence opérationnelle, CQ alignées.
- ⚠️ **Ne pas écrire dans `autogen` (DEFAULT, rétention ∞).** C'est le vrai risque
de croissance (pas un shard runaway : `nymeatest` n'a que 2 shards aujourd'hui).
Router explicitement les writes ETM vers les RP tiers.
- ⚠️ **Version Influx** : `.75` est en **v1.6.7** ✓ (pas 3 Core, pas de piège
72 h). Le tag Docker `latest` pointe désormais vers **InfluxDB 3 Core**
(rétention 72 h + limite 5 DB en OSS) → **pinner**, ne jamais laisser un upgrade
auto basculer.
- **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
@ -334,9 +363,9 @@ du « Plan de la journé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é.
experience). Dérivation depuis cumuls via §2.1. **Réserve levée** (sonde `.75` :
`totalAcquisition`/`totalReturn` séparés) — dérivable des logs, le plugin n'a
pas à intégrer pour l'historique. ✅ 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é.
@ -349,10 +378,18 @@ du « Plan de la journée »).
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)
### Vérifié sur sonde (`.75`)
- `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.
- ✅ **PowerBalance** : instantané net-signé (`acquisition`), cumuls
`totalAcquisition`/`totalReturn` **séparés** → ratios dérivables par Δ (§2.1).
- ✅ **Influx v1.6.7** (pas 3 Core). DB nymea = `nymeatest`, RP existantes
(`live/minutes/hours/days/discrete`), DEFAULT = `autogen` ∞ (le vrai risque).
### Reste ouvert (vrai)
- **Confirmer les signes sur box réelle** (`.120`) — `.75` est statique/factice
(totaux à 0). L'archi plugin=source rend l'app sign-agnostic, mais valider.
- **Carte de comptage §7.2** : décider produit par produit quels consommateurs
non mesurés (PAC, clim) justifient un compteur matériel pour une optim fine.
- **Gestion reset des compteurs** (§2.1/§7.3/§8.1) : implémenter la détection
Δ < 0 côté plugin **et** dans les CQ de rollup.