docs(prévision PV): §31 — enregistrement pour l'apprentissage, écrêtage détectable ou non
Schéma et clé de forecastLearning, règles NULL, révision « en vigueur à X », tableau détectable / non détectable de l'écrêtage, neige, écriture et base abîmée, limites. Inventaire du mock : pvPlane (paramètres) et solarInverter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2b83c206e4
commit
b1fdff197f
@ -1409,3 +1409,113 @@ restent l'état de leur date, et c'est ici qu'on lit ce qui les corrige.*
|
|||||||
rien d'inventé) + `fault: FORECAST_DATE_UNREADABLE`, `faultParams.fields`. Rien n'est lu dans
|
rien d'inventé) + `fault: FORECAST_DATE_UNREADABLE`, `faultParams.fields`. Rien n'est lu dans
|
||||||
la table. Le schéma déclaré change, de façon additive : `energyError` (requis, toujours rendu),
|
la table. Le schéma déclaré change, de façon additive : `energyError` (requis, toujours rendu),
|
||||||
`o:fault`, `o:faultParams`. `forecastPoints` reste requis, donc `[]` en erreur.
|
`o:fault`, `o:faultParams`. `forecastPoints` reste requis, donc `[]` en erreur.
|
||||||
|
|
||||||
|
## 31. L'enregistrement pour l'apprentissage futur — brief 3 T2 (2026-10-13)
|
||||||
|
|
||||||
|
*État du code. Le §5 tient : le GPL **n'apprend pas**, il conserve la matière. Rien de ce qui suit
|
||||||
|
n'entre dans une décision (§11) ; aucun RPC, aucune notification. Code :
|
||||||
|
`etm/forecast/forecastlearninglog.{h,cpp}`.*
|
||||||
|
|
||||||
|
### 31.1 La table, et pourquoi sa clé n'est pas celle de `forecastPv`
|
||||||
|
|
||||||
|
Même fichier (`energy-forecast.sqlite`, `NymeaSettings::storagePath()`), table à part :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE forecastLearning (
|
||||||
|
targetAt, stepS, siteId, planId, -- PRIMARY KEY (targetAt, siteId, planId)
|
||||||
|
gti, tempC, pW, corrections, transportV, -- ce que le PLAN publiait pour ce pas
|
||||||
|
kwp, performanceRatio, tempCoeffPctPerK, noctC, acLimitW, -- la conversion EN VIGUEUR
|
||||||
|
issuedAt, model, observedAt, -- attributs de la charge utile lue (jamais une clé)
|
||||||
|
sitePlans, sitePlansExpected, -- la somme du site est-elle complète ?
|
||||||
|
dayAheadAt, contentIdDayAhead, contentIdHourAhead, -- renvois vers forecastPv
|
||||||
|
measuredW, measuredSource, measuredMinutes, -- ce que le SITE a mesuré
|
||||||
|
curtailed, snow, valid, writtenAt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`forecastPv` est l'axe des RÉVISIONS** : plusieurs contenus pour une heure, identifiés par leur
|
||||||
|
empreinte (§20). **Ici le pas est ÉCOULÉ** : il ne reste qu'un fait par plan, et il ne se révise
|
||||||
|
pas. La clé est donc `(targetAt, siteId, planId)`, et une ligne écrite n'est **jamais réécrite**
|
||||||
|
(`ON CONFLICT … DO NOTHING` — et non `INSERT OR IGNORE`, qui avalerait aussi une violation de
|
||||||
|
`NOT NULL`, en silence).
|
||||||
|
|
||||||
|
Les révisions ne sont pas recopiées : la table y **renvoie** par la clé de contenu du §20,
|
||||||
|
`(targetAt, siteId, contentId)`. Jamais par `issuedAt`, qui n'identifie pas la courbe (§20, §21).
|
||||||
|
`siteId` est dans la clé pour la raison du §20.2 : changer de site désigné ne mêle pas deux
|
||||||
|
historiques.
|
||||||
|
|
||||||
|
### 31.2 Ce que chaque groupe de colonnes affirme — et ce qui y est NULL
|
||||||
|
|
||||||
|
- **`pW`, `gti`, `tempC`** : la **dernière** lecture de la charge utile du plan pour ce pas
|
||||||
|
(`observedAt`). Pour un pas écoulé, c'est le run le plus récent — la meilleure estimation du ciel
|
||||||
|
de cette heure-là. En **v1**, `gti` et `tempC` sont **NULL**, jamais 0. Un `gti[]` ou `tempC[]`
|
||||||
|
de longueur différente de `p` est écarté en entier : l'alignement se ferait par devinette.
|
||||||
|
- **`kwp`, `performanceRatio`, `tempCoeffPctPerK`, `noctC`** : lus **sur le Thing plan**, par leur
|
||||||
|
nom (paramètres puis réglages) — la source unique du §28 décision 1, **jamais recopiée** côté
|
||||||
|
`ems`. `tempCoeffPctPerK`/`noctC` sont **NULL en v1** même si le Thing les porte : la v1 ne les a
|
||||||
|
pas appliqués. Ils sont aussi NULL tant qu'openmeteo ne les déclare pas (brief 2 non déployé).
|
||||||
|
`acLimitW` est le plafond **du site** en vigueur, appliqué après la somme (§28 décision 2).
|
||||||
|
- **`contentIdDayAhead`** : la révision du site en vigueur à **J−1 18:00 locale** (`dayAheadAt`,
|
||||||
|
stocké : le fuseau peut changer). « J−1 » est la veille du jour **local** où le pas commence ;
|
||||||
|
18:00 est un réglage, interprété dans le fuseau du système. **`contentIdHourAhead`** : en vigueur
|
||||||
|
à `targetAt − 1 h`. NULL quand aucune révision n'existait.
|
||||||
|
« En vigueur à X » = parmi les contenus apparus avant X et encore vus à X, le plus récemment
|
||||||
|
apparu ; à défaut, le dernier vu avant X. **Ce n'est pas la règle de `courbe()`**
|
||||||
|
(`MAX(lastSeenAt)`) : pour un X passé, celle-ci désigne un contenu revu *après* X — le cas du
|
||||||
|
§23.2. La limite du §23.2 demeure (`lastSeenAt` mis à jour en place).
|
||||||
|
- **`measuredW`** : moyenne, sur le pas, des échantillons **d'une minute** du bilan de puissance de
|
||||||
|
l'amont (`EnergyLogs::powerBalanceEntryAdded`, `SampleRate1Min` — chacun déjà une moyenne
|
||||||
|
pondérée dans le temps), signe retourné : **positif = produit**. `measuredSource` vaut
|
||||||
|
`power-balance-1min`. Publiée seulement si ≥ 90 % des minutes du pas sont arrivées
|
||||||
|
(`measuredMinutes` le dit) **et** si le site a au moins un Thing `smartmeterproducer` — sans lui,
|
||||||
|
la « production » du bilan est un **zéro de construction**, et elle reste NULL.
|
||||||
|
- **`valid`** : *aucune raison CONNUE d'écarter la ligne* — mesure publiée, `pW` présent,
|
||||||
|
`curtailed` ≠ 1, `snow` ≠ 1, et tous les plans du site présents pour ce pas
|
||||||
|
(`sitePlans == sitePlansExpected`, sans quoi la mesure du site se compare à une somme
|
||||||
|
partielle). Ce n'est **pas** un certificat : voir 31.3.
|
||||||
|
|
||||||
|
### 31.3 L'écrêtage — ce qui est détectable avec les données présentes, et ce qui ne l'est pas
|
||||||
|
|
||||||
|
`curtailed` vaut **1** (détecté), **0** (cherché, rien vu) ou **NULL** (rien ne permettait de
|
||||||
|
chercher). **0 ne veut jamais dire « certainement pas écrêté ».**
|
||||||
|
|
||||||
|
| cause | détectable ? | comment / pourquoi |
|
||||||
|
|---|---|---|
|
||||||
|
| **plafond AC du site** (`acLimitW` déclaré) | **OUI** | une minute du pas à ≥ 98 % du plafond ⇒ l'onduleur bornait, la mesure sous-estime le potentiel ⇒ 1. Mesure couverte, aucune minute au plafond ⇒ 0. |
|
||||||
|
| plafond onduleur **non déclaré** | **NON** | sans `acLimitW`, rien à quoi comparer : `curtailed` reste NULL. Un plateau « stable » ne suffit pas — un midi de ciel clair est aussi plat, et deviner le plafond serait inventer une détection. |
|
||||||
|
| **limitation d'injection** (zéro-injection, limite d'export) | **NON** | `ems` n'a aucun réglage de limite d'export, et aucune interface nymea ne la publie. Une injection à ~0 W se lit aussi bien comme une maison qui consomme tout. |
|
||||||
|
| **consigne onduleur** (limitation de puissance active, SunSpec 123 `WMaxLimPct`, Huawei…) | **NON** | les registres existent chez certains fabricants, mais **aucun Thing ne les expose** : `smartmeterproducer` ne porte que `currentPower` et `totalEnergyProduced`. Il faudrait un état publié par le plugin onduleur — à décider, pas à deviner. |
|
||||||
|
| délestage réseau / coupure onduleur | **NON** | indiscernable d'une production nulle. |
|
||||||
|
|
||||||
|
### 31.4 La neige
|
||||||
|
|
||||||
|
`snow` = une **chute** de neige **lue** sur le Thing site pendant le pas (état `snowfall`,
|
||||||
|
une valeur de **modèle** météo, > 0 ⇒ 1, = 0 ⇒ 0) ; NULL si le site n'a pas cet état. **La neige
|
||||||
|
qui COUVRE les panneaux après la chute n'est pas détectable** : rien ici ne la mesure, et la
|
||||||
|
déduire d'un écart prévu/mesuré serait précisément l'apprentissage qui n'a pas sa place dans le GPL.
|
||||||
|
|
||||||
|
### 31.5 Écriture, rétention, base abîmée
|
||||||
|
|
||||||
|
- **Jamais bloquante** : le cycle (`update()`, après `lireCourbePrevision()`) ne fait que copier en
|
||||||
|
mémoire ; une fois le pas fini (+ 120 s de grâce pour la dernière minute), un lot part vers un
|
||||||
|
**fil d'écriture** qui a **sa** connexion SQLite et attend lui-même un verrou (10 s). Éprouvé
|
||||||
|
base verrouillée : l'appel rend la main en moins de 50 ms.
|
||||||
|
- **Un pas écoulé avant le premier cycle n'est jamais écrit** : il n'a été ni lu ni mesuré par
|
||||||
|
nous. Un **filigrane** par (site, plan) empêche de reprendre un pas déjà écrit — la charge utile
|
||||||
|
garde les heures passées du jour (défaut trouvé par un test, corrigé avant commit).
|
||||||
|
- **Rétention 400 jours**, sur l'instant du cycle, à chaque lot.
|
||||||
|
- **Base abîmée** : table de forme inattendue ou qui échoue au `quick_check` ⇒ **refaite**, son
|
||||||
|
contenu perdu, journalisé. Si ses pages sont trop abîmées pour un `DROP`, elle est **mise de
|
||||||
|
côté** par renommage (`forecastLearning_abimee_<ms>`) et une table neuve prend sa place. Table
|
||||||
|
disparue entre deux lots ⇒ recréée, lot rejoué. Fichier qui n'est pas une base ⇒ enregistrement
|
||||||
|
**hors service**, journalisé une fois ; le service et l'arbitrage continuent. Le fichier n'est
|
||||||
|
**pas** recréé : il porte aussi `forecastPv`.
|
||||||
|
|
||||||
|
### 31.6 Ce qui n'est pas fait, et pourquoi
|
||||||
|
|
||||||
|
- **Aucune lecture RPC** : le brief ne la demande pas, et l'apprentissage lira la base. La forme de
|
||||||
|
la table est le contrat ; elle est ici.
|
||||||
|
- **Les paramètres sont lus au moment de la lecture de `p`, pas transportés avec lui.** Entre une
|
||||||
|
modification d'un paramètre sur le Thing et le recalcul du producteur, une ligne peut porter le
|
||||||
|
nouveau paramètre avec l'ancien `p`. La durée de cette fenêtre dépend du moment où le producteur
|
||||||
|
recalcule après une reconfiguration — **non vérifié** ici. La refermer demande que le producteur
|
||||||
|
publie ses paramètres **dans** la charge utile.
|
||||||
|
|||||||
@ -216,3 +216,15 @@ le critère aurait enfin quelque chose à trier.
|
|||||||
**C'est la condition d'existence de l'étape 4**, pas un confort : décider de rejouer une commande
|
**C'est la condition d'existence de l'étape 4**, pas un confort : décider de rejouer une commande
|
||||||
demande de savoir si des commandes se perdent, et cette question n'est pas posable sur un banc
|
demande de savoir si des commandes se perdent, et cette question n'est pas posable sur un banc
|
||||||
qui ne mesure qu'une charge.
|
qui ne mesure qu'une charge.
|
||||||
|
|
||||||
|
## 9. Prévision PV — `pvPlane` et `solarInverter` (2026-10-13)
|
||||||
|
|
||||||
|
| Classe | Porte | Ce qu'elle ouvre |
|
||||||
|
|---|---|---|
|
||||||
|
| `pvPlane` | état `forecastCurveTransport` ; paramètres `kwp`, `performanceRatio`, `tempCoeffPctPerK`, `noctC` *(paramètres ajoutés au brief 3 T2)* | une courbe de prévision par le RPC (D2) ; la lecture des paramètres de conversion **sur le Thing**, par leur nom |
|
||||||
|
| `solarInverter` | interface `smartmeterproducer` (`currentPower`, `totalEnergyProduced`) *(ajoutée au brief 3 T2)* | le seul producteur du mock : sans lui, « le site a-t-il un producteur ? » valait toujours non, et la production mesurée de l'enregistrement pour l'apprentissage n'était jamais publiée |
|
||||||
|
|
||||||
|
**Ce qui reste introuvable ici** : un Thing **site** portant `snowfall` (le compteur fait office de
|
||||||
|
site, il ne l'a pas — `snow` n'est éprouvé qu'en unitaire), et un plan **sans** les paramètres v2
|
||||||
|
(la classe les porte toujours — le cas « openmeteo d'avant le brief 2 » n'est éprouvé qu'en
|
||||||
|
unitaire, par `lirePlan()`).
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user