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
|
||||
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.
|
||||
|
||||
## 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
|
||||
demande de savoir si des commandes se perdent, et cette question n'est pas posable sur un banc
|
||||
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