diff --git a/docs/DESIGN_PREVISION_PV.md b/docs/DESIGN_PREVISION_PV.md index 15c9432..3d6fdbe 100644 --- a/docs/DESIGN_PREVISION_PV.md +++ b/docs/DESIGN_PREVISION_PV.md @@ -1,6 +1,10 @@ # Prévision de production PV — une série, et elle est révisée -> ## ⛔ BLOQUÉ — le §1 contredit un invariant du dépôt +> ## ✅ TRANCHÉ le 2026-09-08 — `ems` n'appelle pas. Voir §12. +> +> *L'encadré ci-dessous est conservé comme historique de la décision.* +> +> ## ⛔ (résolu) — le §1 contredisait un invariant du dépôt > > **Ce document propose que `ems` appelle lui-même une API distante. C'est interdit ici**, et > l'interdiction est explicite à quatre endroits : @@ -276,3 +280,104 @@ plan, ne pondère pas l'étalement éco, ne déplace aucune échéance. > décision** — et le **§10** est aujourd'hui la seule place où elle mordrait : c'est la cadence > d'obligation qui achèterait moins ce matin en pariant sur cet après-midi. Un pari, sur une > courbe dont le §3 dit qu'elle est un **plafond** systématique. À ne pas glisser dans ce lot-ci. + + +--- + +## 12. Le découpage tranché, et le canal — 2026-09-08 + +**`ems` n'appelle pas l'API. L'invariant 10 reste tel quel**, et la raison est plus fine que +« zéro réseau » : + +> Un **plugin d'intégration** qui lit une API distante perd **une entrée** quand le lien tombe, +> comme un capteur perd la sienne. Un **experience-plugin** qui appelle le réseau, c'est **la +> logique de décision** qui se met à dépendre d'un service distant. L'invariant est écrit dans ce +> dépôt-ci pour cette raison, et il est juste là où il est. + +L'attribution CC BY 4.0 reste donc chez openmeteo, où elle est déjà écrite : **le problème +disparaît avec le découpage au lieu d'être géré.** + +### 12.1 Le précédent cité ne couvre PAS ce lot — vérifié dans l'arbre amont + +`awattar`, `tempo`, `spothinta` sont des précédents d'**intégrations qui appellent une API**. +Ils ne sont **pas** des précédents de **série transmise au moteur**. Le plugin `awattar` de +l'amont n'expose que des **scalaires** : + +``` +currentMarketPrice · averagePrice · lowestPrice · highestPrice · rank · validUntil +``` + +**La série ne traverse jamais.** L'amont a résolu le problème en **appelant deux fois** : notre +propre `energyplugin/spotmarket/spotmarketdataproviderawattar.cpp` refait la requête vers +`https://api.awattar.at/v1/marketdata`. + +> **Il n'existe donc aucun canal éprouvé pour une série, parce que personne n'en a construit.** +> Ce lot en ouvre un ; il n'en reprend pas un. + +### 12.2 Conséquence à ne pas laisser sans suite : le moteur appelle DÉJÀ + +`SpotMarketDataProviderAwattar` vit dans **ce dépôt**, sort en HTTPS vers `api.awattar.at` / +`api.awattar.de`, et son résultat alimente `planSpotMarketCharging()` — **une décision**, pas un +affichage. + +C'est exactement ce que l'invariant 10 interdit, selon le raisonnement qui vient de le confirmer, +et **c'est en service**. Hérité de l'amont, jamais choisi — mais un invariant qui a une exception +non déclarée n'est plus un invariant, c'est une habitude. **À trancher séparément** : soit +l'exception est écrite avec sa raison, soit le fournisseur spot descend chez une intégration. + +### 12.3 Le canal : un état portant du JSON — les trois grandeurs, mesurées + +| question | réponse | source | +|---|---|---| +| **taille supportable** | **1 MiB**, plafond dur du transport | `jsonrpcserverimplementation.cpp:554` — au-delà, la connexion est **larguée** | +| **coût de rediffusion** | **par RÉVISION, pas par cycle** | `thing.cpp` : `if (oldValue == newValue) return;` — nymea **dédoublonne** | +| **nommage** | préfixe `forecast`, et le mot dit que c'est un canal | §7 | + +Une courbe de 48 h fait **2 à 5 ko** : trois ordres de grandeur sous le plafond. **La taille n'est +pas le sujet.** Et réécrire la même valeur à chaque poll n'émet **rien** — le coût est d'une +diffusion par heure, pas de soixante. + +> La prémisse « rediffusion à chaque cycle » surestime d'un facteur 60. Ce qui compte n'est pas +> la cadence du moteur, c'est celle des **révisions**. + +### 12.4 Les deux vrais dangers, et aucun n'est la taille + +**a. `cached` — la réécriture du fichier de configuration.** Un état `cached` est **écrit sur +disque** à chaque changement (`thingmanagerimplementation.cpp` → `storeThingStates()`). Un état +`cached` portant plusieurs kilo-octets de JSON réécrit `things.conf` **toutes les heures**, sur +une carte SD. **`"cached": false` est obligatoire**, comme l'état `connected` d'`awattar`. + +**b. La journalisation d'état — une base de données créée par une case à cocher.** Si quelqu'un +active le logging sur cet état (`Integrations.SetStateLogging`, exposé dans l'app), **chaque +révision écrit une copie complète du JSON dans `energylogs.sqlite`**. Personne ne l'aura décidé, +rien ne le signalera, et la base grossira jusqu'à ce que le disque le dise. + +> C'est le mode d'érosion de l'instrument dont le silence ressemble à un résultat, retourné : +> **un mécanisme qu'un tiers arme sans savoir ce qu'il arme.** Le nom doit l'en dissuader, et le +> contrat doit dire que cet état ne se journalise pas. + +### 12.5 Ce que je vois de plus : un JSON qui traverse n'a plus de schéma + +Au moment où la courbe devient une chaîne, **elle n'est plus typée**. Si openmeteo change la +forme du contenu, `ems` casse **à l'exécution**, sans qu'aucune compilation ni aucun contrat ne +l'ait signalé — et le symptôme sera « la courbe est vide », c'est-à-dire indistinguable d'une nuit. + +**Un numéro de version DANS la charge utile, et le refus plutôt que l'interprétation au mieux.** +Une version inconnue produit **aucune courbe** et un avertissement nommé, jamais une lecture +partielle qui aurait l'air de marcher. + +C'est le versant charge utile de la règle déjà écrite au contrat : *un contrat qu'aucun appelant +n'exerce n'est pas un contrat*. Ici, c'est un contrat qu'aucun compilateur ne vérifie — donc il +faut que quelqu'un le vérifie à l'exécution, une fois, à l'entrée. + +### 12.6 Nommage proposé + +| | | +|---|---| +| état | `forecastPvCurve` — **`cached: false`**, non journalisable | +| charge utile | `{ "v": 1, "issuedAt": , "points": [{ "t": , "wh": }] }` | + +`v` d'abord, `issuedAt` ensuite : les deux choses à lire **avant** de croire le reste. Et +`issuedAt` est **dans la charge utile**, pas déduit de l'instant de réception — sinon un +redémarrage de `ems` réétiquette toute la série avec l'heure du redémarrage, et la table +bitemporelle perd exactement ce qu'elle existe pour retenir.