docs(prévision PV): le canal — trois grandeurs mesurées, et le précédent ne couvre pas ce lot
Découpage tranché : ems n'appelle pas, l'invariant 10 reste. La distinction retenue est plus fine que « zéro réseau » — une intégration qui lit une API perd UNE ENTRÉE quand le lien tombe ; un experience-plugin qui appelle met LA LOGIQUE DE DÉCISION sous dépendance distante. L'attribution CC BY 4.0 disparaît avec le découpage au lieu d'être gérée. LE PRÉCÉDENT CITÉ NE COUVRE PAS CE LOT. awattar/tempo/spothinta sont des précédents d'intégrations qui APPELLENT. Le plugin awattar amont n'expose que des SCALAIRES — currentMarketPrice, average, lowest, highest, rank, validUntil. La série ne traverse jamais : l'amont a résolu le problème en appelant DEUX FOIS. Il n'existe donc aucun canal éprouvé pour une série, parce que personne n'en a construit. ET LE MOTEUR APPELLE DÉJÀ : spotmarketdataproviderawattar.cpp, dans CE dépôt, sort en HTTPS vers api.awattar.at et alimente planSpotMarketCharging() — une DÉCISION. C'est ce que l'invariant 10 interdit, en service, hérité et jamais choisi. Un invariant qui a une exception non déclarée n'est plus un invariant, c'est une habitude. À trancher séparément. Les trois grandeurs, sur le code et non la doc : - taille : plafond dur à 1 MiB (jsonrpcserverimplementation.cpp:554, connexion larguée au-delà). Une courbe 48 h fait 2-5 ko — la taille n'est pas le sujet ; - rediffusion : PAR RÉVISION, pas par cycle. thing.cpp dédoublonne (if (oldValue == newValue) return). La prémisse surestimait d'un facteur 60 ; - nommage : forecastPvCurve, et le mot dit que c'est un canal. Les deux vrais dangers ne sont pas la taille : « cached » réécrirait things.conf toutes les heures sur une carte SD (cached: false obligatoire), et la JOURNALISATION D'ÉTAT créerait une base de données par une case à cochée dans l'app — chaque révision copiant le JSON entier dans energylogs.sqlite, sans que personne l'ait décidé. Et ce que je vois de plus : un JSON qui traverse n'a plus de schéma. Un numéro de version DANS la charge utile, avec refus plutôt qu'interprétation au mieux — sinon le symptôme d'un changement de forme est « courbe vide », indistinguable d'une nuit. issuedAt vient aussi de la charge utile, jamais de l'instant de réception : sinon un redémarrage réétiquette la série et la table bitemporelle perd ce qu'elle existe pour retenir. 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
5499e88ac3
commit
56312a7585
@ -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": <epoch UTC>, "points": [{ "t": <epoch UTC>, "wh": <double> }] }` |
|
||||
|
||||
`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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user