From 04fea6ffdaf6dc29da21d665fed73a353218988b Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 14 Oct 2026 10:05:43 +0200 Subject: [PATCH] =?UTF-8?q?docs(pr=C3=A9vision):=20=C2=A732=20consommation?= =?UTF-8?q?=20de=20base=20et=20prix=20;=20protocole=20=C2=A75.2=20(tou,=20?= =?UTF-8?q?quality)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design §32 : grille, mesure à la minute, profil, qualité, tarif mural, règle du créneau à cheval (moyenne pondérée) et ce qui reste pour e1. Protocole 1.1, correction avant publication : price.source hphc → tou (jamais émis), baseload.quality optionnel. Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN_PREVISION_PV.md | 81 +++++++++++++++++++++++++++++++++++++ docs/OPTIMIZER_PROTOCOL.md | 15 ++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/docs/DESIGN_PREVISION_PV.md b/docs/DESIGN_PREVISION_PV.md index b57deeb..b43c8d2 100644 --- a/docs/DESIGN_PREVISION_PV.md +++ b/docs/DESIGN_PREVISION_PV.md @@ -1598,3 +1598,84 @@ S'y ajoutent les sondages en échec (pas de nouvel essai avant l'heure suivante) redémarrages. La vraie valeur dépend du rythme réel de mise à jour du produit satellite, qui n'est pas mesuré ici. Elle se lira sur le banc : la part des lignes de jour dont `gtiObserved` est NULL. **Rien n'est comblé côté `ems`** : la correction est de publier la série, ce sera openmeteo etm16. + +## 32. Consommation de base et prix sur la grille — brief 3 T4 (2026-10-14) + +*État du code. Rien de ce qui précède n'est réécrit. Aucune décision ne lit ce qui suit (§11) : +`SurplusContext::tariff` n'est pas rempli, la base n'entre dans aucun contexte. Épinglé par +`testNoDecisionReadsBaseloadOrPrice` (même cycle, mêmes entrées, même plan, avec et sans tarif ni +profil). Contrat : `INTERFACE.md`, « Tarif saisi » et `GetForecastSnapshot`. Code : +`etm/tariff/tariffconfig.{h,cpp}`, `etm/forecast/baseloadlog.{h,cpp}`.* + +### 32.1 La grille est celle de `pv` — et elle existe sans `pv` + +`GetForecastSnapshot` publie désormais `start`, `resolutionS`, `steps` (protocole §5.2), et sur +cette grille `baseload` et `price`, alignés index par index. Avec une courbe, c'est **sa** grille +(premier → dernier créneau, trous compris). **Sans courbe, la grille par défaut du producteur** +(horaire, minuit local → minuit local du surlendemain) : un site non désigné ne doit pas faire +disparaître un tarif saisi. Le manque « le pas n'est publié par aucune méthode » est fermé pour +le snapshot. + +### 32.2 La base — mesurée à la minute, jamais sur les cumuls + +- **Source** : l'échantillon d'**une minute** du bilan (`powerBalanceEntryAdded`, `SampleRate1Min`), + ses **puissances moyennes** — `acquisition + |production| − storage`, la formule de l'amont pour + `consumption`, réécrite depuis ses termes. **Jamais** `totalConsumption`/`totalProduction`/ + `totalAcquisition`/`totalReturn` : 63 117 kWh de production cumulée au bilan contre 4 914 kWh au + SDM120 (relevé du 2026-10-13). ⚠️ Le terme `− storage` n'est pas dans la formule du brief + (« import − export + production ») : sans batterie il vaut 0 ; avec, une charge de batterie + n'est pas une consommation. +- **Retranché** : chaque charge pilotée (tables de l'arbitre, **bornes comprises**) selon **sa** + `measurement.source` — la même fonction que la télémétrie (`mesurerCharge()`, extraite pour + qu'il n'y ait qu'une règle). Sous `none`, la **puissance commandée** (`puissanceCommandee()`, + extraite de l'écart commandé/mesuré : la même règle). Une commande inconnue rend la minute + **incomplète**, jamais comptée à 0. +- **Pas de minute** sans compteur racine (import = zéro de construction) ni en mode dégradé L2 + (le compteur est muet). +- **Négatif gardé** : une base < 0 dit que le compteur racine voit moins que les charges — le + constat du banc (« ~1,5 kW de moins », `AGENTS.md`). L'écrêter cacherait ce que la qualité doit + montrer. + +**Stockage** — deux tables dédiées dans `energy-forecast.sqlite` : `baseloadMinute (at, siteW, +loadsW, baseW, commandedLoads, writtenAt)` et `baseloadMinuteLoad (at, loadId, source, powerW)`. +`forecastPv` et `forecastLearning` ne sont **pas touchées** (éprouvé sur le fichier etm67 réel : +schéma et lignes identiques). Fil d'écriture dédié (sa connexion, `busy_timeout` 10 s), lots de +5 minutes, lecture de la fenêtre **par journée** pour ne jamais tenir le verrou du fichier +longtemps (le cycle y écrit les révisions). Rétention **35 jours** (fenêtre + une semaine). Forme +inattendue d'une de NOS tables ⇒ refaite (aucun historique antérieur n'existe) ; fichier illisible +⇒ hors service, journalisé une fois. + +### 32.3 Le profil + +Médiane, par case de la journée **locale** (largeur = pas de la grille) et par type de jour +(ouvré / week-end, fériés inconnus), des **moyennes de case** couvertes à ≥ 90 %, sur **28 jours**. +Une case sous **2** valeurs est `null`. **`none` sous 7 jours** couverts à ≥ 90 % de **leurs** +minutes (1 380 / 1 440 / 1 500). Un créneau de grille prend la case de l'heure murale de son +début (25 h : les deux « 02:00 » ont la même médiane ; 23 h : la case 02:00 n'est pas lue). +Recalcul sur le fil au plus tous les quarts d'heure (temps des minutes) et à l'ouverture ; le +snapshot lit la dernière copie, sous verrou bref, sans requête. + +**La qualité dit ce que le modèle ne peut pas cacher** : minutes comptées à la **commande**, +minutes **négatives**, minutes **écartées**, et par charge sa source et `constantMeasurement` +(mesure qui n'a pas bougé d'un watt sur ≥ 60 minutes — la « constante déguisée » du §6 de +`DESIGN_COMMANDE_PERDUE.md`). Sur le banc, au 2026-10-14 : chauffe-eau `none` (compté à sa +commande), PAC sur `PAC-Meter` figé → `constantMeasurement: true`. + +### 32.4 Le tarif — un réglage mural, rendu en instants + +Saisi par l'installateur (décision du 2026-10-14) : **une** zone 00:00-24:00 (`fixed`) ou des zones +horaires (`tou`), chacune `{label, priceCtkWh, ranges[]}`, plusieurs plages par zone. Validation +**stricte** (`TariffConfig::fromVariant()`, la même pour le RPC et la relecture) : prix ≥ 0, heures +`"HH:MM"`, couverture des 1 440 minutes **sans trou ni chevauchement**, refus codé et localisé +(`TARIFF_REFUSED`, `cause` + index + minute). Persisté dans `energy.conf`. + +**Créneau à cheval : moyenne pondérée par la durée — proposé et retenu.** Toutes les séries de la +grille sont des moyennes sur `[t, t + pas)` ; pour une puissance constante sur le créneau, le coût +est exactement `P × pas × moyenne pondérée`. C'est donc la seule valeur cohérente avec `pv` et +`baseload`. Le découpage au pas changerait la grille, qui est une pour toutes les séries. Calcul : +minute **absolue** par minute, chaque minute prend la zone de son heure murale — exact pour les +jours de 23 et 25 h (oracles Python `zoneinfo` dans les tests). + +**Pour e1 (plus tard, sans être lu maintenant)** : `TariffConfig::zoneAt(instant, fuseau)` rend la +zone en vigueur ; « HC » n'a pas de code — le libellé est une donnée de l'installateur. Le lot e1 +devra décider d'une règle (par exemple : la ou les zones au prix le plus bas), pas la deviner ici. diff --git a/docs/OPTIMIZER_PROTOCOL.md b/docs/OPTIMIZER_PROTOCOL.md index b32784d..9c9e776 100644 --- a/docs/OPTIMIZER_PROTOCOL.md +++ b/docs/OPTIMIZER_PROTOCOL.md @@ -40,6 +40,10 @@ optimiseur 1.0 reste conforme (champs inconnus ignorés, §9). rév. 3, avant toute implémentation socket). Aucun consommateur déployé n'en dépend ; ils sont retirés du document sans changement de majeure. +**Correction avant publication (2026-10-14)** : `price.source` `hphc` devient `tou` (zones +horaires saisies, N zones et plusieurs plages par zone — HP/HC en est un cas) ; `hphc` n'a +jamais été émis. `baseload.quality` est ajouté (optionnel). + **Correction avant publication (2026-10-13)** : `conversion` n'admet plus `fallback`. La valeur avait été écrite sans définition ; **aucun émetteur ne l'a jamais produite**. Seule `physics` est émise ; une prévision dégradée se lit dans `corrections`. @@ -266,9 +270,18 @@ Règles : depuis le compteur racine moins les charges, chacune selon `measurement.source` (§5.3) ; une charge en `none` est comptée à sa puissance **commandée**, et la qualité de l'estimation le dit. + - `profile` (plugin GPL) : médiane par créneau local et par type de jour (ouvré / week-end) + sur 28 jours, mesurée sur les échantillons d'une minute du bilan — jamais ses cumuls ; + `none` sous 7 jours couverts. Forme et définition : `INTERFACE.md`, `GetForecastSnapshot`. + - `quality` (1.1, optionnel) : codes et nombres — jours couverts, minutes comptées à la + commande, minutes négatives, et par charge sa source et `constantMeasurement`. - **`price`** : **prix complet payé** — `includes` liste ce qui est inclus. Un prix n'est - **pas une prévision** : au-delà de `knownUntil`, `null`. Sources : `fixed`, `hphc`, + **pas une prévision** : au-delà de `knownUntil`, `null`. Sources : `fixed`, `tou`, `tempo`, `day-ahead`. `exportCtkWh` : tarif de revente ou 0. + - `fixed` / `tou` : tarif **saisi** (une zone sur 24 h / zones horaires en heure locale) ; + connu sur tout l'horizon, `knownUntil` = fin de la grille. Un créneau à cheval sur deux + zones reçoit la **moyenne pondérée par la durée** — le coût exact d'une puissance + constante sur le créneau, seule valeur cohérente avec les autres séries de la grille. - `forecast` peut être `null`. L'optimiseur reste libre de sourcer ses propres prévisions (pont LAN, côté optimiseur), mais **le plugin, lui, ne sort pas** pour les obtenir.