docs(design): la prévision PV est une série RÉVISÉE — table bitemporelle
Nouveau lot cadré par Patrick. Le plugin openmeteo reste météo pure ; les plans PV, la conversion, l'appel API et l'historique vivent ici. LE POINT DE CONCEPTION : la table n'est pas « valeur par heure » mais « valeur par heure, telle que prévue à tel moment ». La clé est la PAIRE (targetAt, issuedAt). Avec targetAt seul, chaque rafraîchissement écrase le précédent : on perd les révisions, donc la comparaison prévu/réalisé — ce qu'on veut précisément montrer. Même distinction que measuredSince. Conséquence de contrat que le cadrage n'énonçait pas : « prévu » ne veut rien dire sans HORIZON. Trois lectures (courante, veille, H−1) répondent à trois questions ; publier « prévu » sans dire laquelle laisse deux écrans montrer deux courbes pour le même jour. Repris de l'agent openmeteo, mesuré et non supposé : indexation rétrograde ; cinq facteurs ignorés, TOUS optimistes — donc un biais signé, pas un bruit : la conversion produit un PLAFOND, pas une estimation ; la dispersion d'ensemble est ANTICORRÉLÉE au biais, donc un indicateur de confiance qui rassure le plus quand il se trompe le plus — pire que pas d'indicateur, et l'affiner l'empire ; et la correction prévu↔mesuré est un apprentissage, hors GPL. Le GPL n'apprend pas, il rend l'apprentissage POSSIBLE : la table bitemporelle EST l'interface avec Héos, et l'écraser à chaque révision détruirait la matière pour toujours sans que rien ne le signale. Cinq ajouts au cadrage : targetAt en epoch UTC jamais en heure murale (le piège du 6 septembre appliqué à une série entière — vérification sur des jours de 23 h et 25 h) ; rétention 7 jours pleins puis effondrement aux DEUX bouts, parce que n'en garder qu'un détruit la révision ; stockage chez nous et non dans energylogs.sqlite, que l'amont migre ; l'absence est un TROU, jamais un zéro ; et l'historique doit retenir SOUS QUEL PLAN une prévision a été convertie, sinon un recâblage rend la série antérieure incomparable en silence. Trois choses manquent pour coder, dont la convention d'indexation AVEC ses paramètres : reprendre une conclusion sans ses prémisses, c'est hériter d'un fait vrai sous des conditions qu'on ne saura pas reproduire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
parent
5dfd8b9047
commit
5316a6f723
219
docs/DESIGN_PREVISION_PV.md
Normal file
219
docs/DESIGN_PREVISION_PV.md
Normal file
@ -0,0 +1,219 @@
|
||||
# Prévision de production PV — une série, et elle est révisée
|
||||
|
||||
> Ouvert le 2026-09-08. **Design avant code, rien d'implémenté.**
|
||||
> Cadrage donné par Patrick. Les constats météo viennent de l'agent openmeteo et sont
|
||||
> **repris tels quels** : ils sont mesurés, pas supposés.
|
||||
|
||||
## 1. La frontière, et pourquoi elle tombe là
|
||||
|
||||
| | responsabilité |
|
||||
|---|---|
|
||||
| plugin **openmeteo** | **météo pure** — irradiance, température, couverture. Rien de photovoltaïque |
|
||||
| **ems** (ici) | les **plans PV** (crête, orientation, inclinaison), la **conversion**, l'**appel API**, et l'**historique** |
|
||||
|
||||
Le plugin météo ne connaît pas l'installation ; l'installation ne se déclare pas dans un plugin
|
||||
météo. Et c'est ici que vivent déjà la configuration persistée, le compteur de production et la
|
||||
frontière RPC — donc ici que la série peut être servie à l'app sans traverser une troisième
|
||||
maison.
|
||||
|
||||
### Pourquoi ce n'est pas un état nymea
|
||||
|
||||
**Le besoin n'est pas de décider, c'est d'afficher** : la courbe prévue d'aujourd'hui et de J+1,
|
||||
superposée à la production réelle.
|
||||
|
||||
- **Aucun état nymea ne porte une série.** Un état est une valeur à un instant.
|
||||
- **`suggestLogging` ne construit que du passé.** Il enregistre ce qu'un état a valu.
|
||||
**Une prévision porte sur des heures qui ne sont pas arrivées** — il n'y a rien à enregistrer,
|
||||
il y a quelque chose à **écrire d'avance**.
|
||||
|
||||
C'est le même geste que le logging natif pour les compteurs, à ceci près qu'il regarde devant.
|
||||
|
||||
## 2. LE POINT DE CONCEPTION — la table est BITEMPORELLE
|
||||
|
||||
**Une prévision est révisée.** Celle de 8 h pour 15 h n'est pas celle de 14 h pour 15 h.
|
||||
|
||||
La table n'est donc **pas** « valeur par heure ». Elle est **« valeur par heure, telle que prévue
|
||||
à tel moment »** :
|
||||
|
||||
```sql
|
||||
CREATE TABLE forecastPv (
|
||||
targetAt INTEGER NOT NULL, -- epoch UTC de l'heure VISÉE
|
||||
issuedAt INTEGER NOT NULL, -- epoch UTC de l'ÉMISSION de cette prévision
|
||||
forecastWh REAL NOT NULL,
|
||||
model TEXT,
|
||||
PRIMARY KEY (targetAt, issuedAt) -- la CLÉ EST LA PAIRE
|
||||
);
|
||||
```
|
||||
|
||||
**La clé est la paire, et c'est tout le lot.** Avec `targetAt` seul, chaque rafraîchissement
|
||||
écrase le précédent : on perd les révisions, donc la comparaison prévu/réalisé — c'est-à-dire
|
||||
exactement ce qu'on veut montrer, et ce dont Héos aura besoin.
|
||||
|
||||
> C'est la même distinction que `measuredSince` : **quand on a mesuré**, et **ce qu'on a mesuré**.
|
||||
> Deux dates, deux questions, et les confondre en perd une.
|
||||
|
||||
### Conséquence de contrat : « prévu » ne veut rien dire sans HORIZON
|
||||
|
||||
Dès que les révisions coexistent, « la prévision pour 15 h » est ambiguë. Toute lecture doit
|
||||
nommer son horizon, et l'écran aussi :
|
||||
|
||||
| lecture | requête | ce qu'elle répond |
|
||||
|---|---|---|
|
||||
| **courante** | pour chaque `targetAt`, la ligne de `issuedAt` maximal ≤ maintenant | « ce qu'on prévoit en ce moment » |
|
||||
| **veille** | `issuedAt` le plus proche de J−1 à heure fixe | « la prévision de la veille valait-elle quelque chose ? » |
|
||||
| **à H−1** | `issuedAt` le plus proche de `targetAt − 1 h` | « la meilleure estimation possible » |
|
||||
|
||||
Les trois répondent à des questions différentes. Publier « prévu » sans dire laquelle laisse le
|
||||
client en choisir une au hasard, et deux écrans montrent deux courbes pour le même jour.
|
||||
|
||||
## 3. Ce que la conversion IGNORE — cinq facteurs, tous optimistes
|
||||
|
||||
Repris de l'agent openmeteo, mesuré :
|
||||
|
||||
1. l'**ombrage**,
|
||||
2. l'**encrassement**,
|
||||
3. la **température de module**,
|
||||
4. l'**écrêtage onduleur**,
|
||||
5. les **salissures**.
|
||||
|
||||
**Les cinq vont dans le même sens.** Ce n'est donc pas un bruit qui se compense : c'est un
|
||||
**biais systématique et signé**. La conversion ne produit pas une estimation, elle produit un
|
||||
**plafond**.
|
||||
|
||||
> **À écrire dans le contrat, pas seulement ici.** Une courbe présentée comme « production
|
||||
> prévue » et systématiquement au-dessus du réel se lit comme une installation qui sous-performe.
|
||||
> L'utilisateur cherchera une panne qui n'existe pas — et il aura raison de chercher, puisque
|
||||
> l'écran lui aura affirmé une chose fausse.
|
||||
|
||||
## 4. La dispersion d'ensemble ne QUALIFIE PAS la production
|
||||
|
||||
Le réflexe serait de publier la dispersion des membres d'ensemble comme un indice de confiance.
|
||||
**C'est exactement l'inverse qu'il faut faire.**
|
||||
|
||||
Elle est **anticorrélée au biais** : un ciel clair et stable donne la **dispersion la plus
|
||||
basse** au moment précis où la **perte thermique est la plus forte** — donc où l'erreur de
|
||||
conversion est la plus grande.
|
||||
|
||||
> **Un indicateur de confiance anticorrélé à l'erreur est pire que pas d'indicateur.** Il est le
|
||||
> plus rassurant exactement quand il se trompe le plus. Il ne s'agit pas d'un indicateur
|
||||
> imparfait qu'on affinerait : il pointe dans la mauvaise direction, et l'affiner l'empire.
|
||||
|
||||
Elle peut être **stockée** (elle décrit le ciel, et Héos en fera peut-être quelque chose). Elle
|
||||
ne doit **jamais être publiée comme une qualité de la prévision de production**.
|
||||
|
||||
*(Famille des modes d'érosion d'`AGENTS.md` : l'instrument dont le silence ressemble à un
|
||||
résultat. Ici, c'est l'instrument dont la confiance ressemble à une garantie.)*
|
||||
|
||||
## 5. Ce qui est HORS GPL — et ce que le GPL doit publier pour que l'autre existe
|
||||
|
||||
**Comparer prévu à mesuré pour en tirer une correction est un apprentissage**, donc hors GPL.
|
||||
|
||||
Mais la conséquence est constructive, et c'est elle qui justifie la table bitemporelle :
|
||||
|
||||
> **Le GPL n'apprend pas ; il rend l'apprentissage POSSIBLE.** La table `(targetAt, issuedAt)`
|
||||
> est l'interface entre les deux. Sans elle, Héos n'aurait pas la matière — et l'écraser à chaque
|
||||
> révision détruirait cette matière pour toujours, sans que rien ne le signale.
|
||||
|
||||
Le GPL publie donc la série et le réalisé côte à côte, **sans les rapprocher**. Le rapprochement
|
||||
est le produit.
|
||||
|
||||
## 6. Cadence — une heure, et le raisonnement est celui du polling d'une borne
|
||||
|
||||
Les modèles se rafraîchissent **au mieux toutes les heures**. Interroger plus souvent brûle du
|
||||
quota sans rien apporter : ce n'est pas une donnée neuve, c'est la même donnée relue.
|
||||
|
||||
Le quota (10 000 appels/jour/IP) est très loin — **et ce n'est pas l'argument**. L'argument est
|
||||
le même que pour la borne : *on ne polle pas plus vite que la chose n'a de nouveau à dire*. Une
|
||||
cadence justifiée par « on a le droit » est une cadence qu'on ne sait pas défendre.
|
||||
|
||||
## 7. Le NOM porte la protection
|
||||
|
||||
**Préfixe `forecast` partout**, sans exception : `forecastProductionW`, `forecastIssuedAt`,
|
||||
`forecastHorizonS`. Jamais `productionW`, jamais une valeur de `measurement.source`, jamais un
|
||||
champ qu'une somme puisse mêler à du mesuré.
|
||||
|
||||
> **C'est la règle qui a coûté le plus cher ces trois semaines.** Un nombre qui ne dit pas d'où
|
||||
> il vient finit toujours par être lu comme une mesure — et une prévision lue comme une mesure
|
||||
> est un mensonge que personne n'a écrit.
|
||||
|
||||
---
|
||||
|
||||
## 8. Ce que j'ajoute au cadrage
|
||||
|
||||
### 8.1 Une heure n'est pas une heure — le piège du 6 septembre, à nouveau
|
||||
|
||||
`targetAt` doit être un **instant absolu en UTC**, jamais un libellé d'heure locale. Un `2026-10-25
|
||||
02:00` local existe **deux fois** et un `2026-03-29 02:00` n'existe **pas** : indexer par heure
|
||||
murale corrompt silencieusement la série deux fois par an, et l'erreur se voit un jour où
|
||||
personne ne regarde.
|
||||
|
||||
Concrètement : demander `timezone=UTC` à l'API, stocker des epochs, **convertir au rendu
|
||||
seulement**. C'est la distinction déjà tranchée le 2026-09-07 — *un réglage est une chaîne qu'on
|
||||
interprète, un instant est absolu* — appliquée à une série entière au lieu d'un champ.
|
||||
|
||||
**Et la vérification qui mord** : deux jours de 23 h et 25 h dans les données de test, avec la
|
||||
somme quotidienne comparée au nombre de lignes. Une série indexée en heure murale y perd ou
|
||||
double une heure, et rien d'autre ne le montre.
|
||||
|
||||
### 8.2 Rétention — la table grandit en heures × révisions
|
||||
|
||||
48 h d'horizon × 24 révisions/jour ≈ **1 150 lignes par jour**, ~46 ko/jour, ~17 Mo/an. Tenable
|
||||
sur la cible, **mais pas indéfiniment**, et une table qui ne dit pas quand elle purge finit par
|
||||
le décider toute seule un jour de disque plein.
|
||||
|
||||
Proposition : **7 jours de révisions complètes**, puis effondrement à **deux lignes par heure
|
||||
visée** — la **première** (horizon le plus long) et la **dernière avant l'heure** (meilleure
|
||||
estimation). Garder les deux bouts conserve les deux seules questions qui se posent : *« la
|
||||
veille valait-elle quelque chose ? »* et *« que pouvait-on savoir de mieux ? »*. N'en garder
|
||||
qu'une détruit la révision, c'est-à-dire l'objet du lot.
|
||||
|
||||
### 8.3 Où vit le stockage : chez nous, pas dans `energylogs.sqlite`
|
||||
|
||||
`energylogs.sqlite` appartient à l'amont, qui en migre le schéma. Y ajouter une table nous
|
||||
exposerait à une migration qui ne nous connaît pas. Un fichier à nous, à côté de
|
||||
`energy-load-configuration.json`, avec **notre** numéro de schéma.
|
||||
|
||||
### 8.4 L'absence n'est pas un zéro
|
||||
|
||||
Pas de réseau, API en erreur, plan PV non déclaré : **aucune ligne**. Jamais `0 Wh`, qui se lirait
|
||||
« on prévoit une nuit ». La courbe publiée porte des **trous**, et l'écran doit savoir les
|
||||
afficher comme tels. Même règle que partout ici, et c'est la plus souvent oubliée quand la
|
||||
donnée est une série : un trou dans une courbe demande un effort de rendu, un zéro n'en demande
|
||||
aucun — et c'est pour ça qu'on écrit des zéros.
|
||||
|
||||
### 8.5 Les plans PV sont une CONFIGURATION, donc ils se versionnent
|
||||
|
||||
Crête, orientation, inclinaison sont saisis par l'installateur, comme les rangs de charge. Même
|
||||
famille que `LoadConfigStore` : store persisté, RPC de lecture/écriture, validation au **refus**
|
||||
et non à la correction silencieuse.
|
||||
|
||||
**Et un plan qui change périme la conversion, pas la météo.** L'historique doit donc retenir
|
||||
*sous quel plan* une prévision a été convertie — sinon un recâblage rend toute la série
|
||||
antérieure incomparable **sans que rien ne le dise**. C'est la même erreur que le plancher de
|
||||
borne qu'on aurait stocké au lieu de le dériver, déplacée dans le temps.
|
||||
|
||||
---
|
||||
|
||||
## 9. Ce qu'il me manque pour écrire le code
|
||||
|
||||
1. **La convention d'indexation, avec ses PARAMÈTRES.** Elle est rétrograde et mesurée — je la
|
||||
reprends. Mais une convention d'indexation dépend de la requête qui l'a produite (variables
|
||||
demandées, `timezone`, horaire vs quart-horaire). Reprendre la conclusion sans ses prémisses,
|
||||
c'est hériter d'un fait vrai sous des conditions qu'on ne saura pas reproduire. **L'énoncé
|
||||
exact et les paramètres de l'appel mesuré**, et je m'aligne dessus.
|
||||
2. **Où se déclarent les plans PV** : un nouveau store à nous (ma proposition, §8.5), ou des
|
||||
champs sur le Thing onduleur ?
|
||||
3. **La position du banc** — latitude/longitude. Elles conditionnent l'appel, et une prévision
|
||||
convertie sur la mauvaise position est fausse d'une façon parfaitement crédible.
|
||||
|
||||
## 10. Étapes proposées, à comportement constant d'abord
|
||||
|
||||
**A** — le store des plans PV et son RPC. Rien n'appelle l'API ; la configuration existe et ne
|
||||
décide de rien.
|
||||
**B** — l'appel Open-Meteo et la table bitemporelle, **sans publication**. La série se remplit et
|
||||
s'observe au banc, y compris en travers d'un changement d'heure.
|
||||
**C** — la publication RPC de la courbe, avec son horizon nommé et ses trous.
|
||||
**D** — la superposition au réalisé, **côte à côte et sans rapprochement**.
|
||||
|
||||
Le pas **D** est celui qui touche à la frontière avec le produit, et il arrive quand la série est
|
||||
déjà vérifiable.
|
||||
Loading…
x
Reference in New Issue
Block a user