Bilan : une heure de nuit n'a aucune forme a minorer
Le drapeau « degrade » introduit la veille s'allumait de 20 h a 07 h toutes les nuits. La qualite de forme d'une heure vient du taux de censure ; les heures sans soleil portent un taux de 1 faute d'avoir quoi que ce soit a observer, pas parce que la mesure serait douteuse. Comme les heures de deficit sont justement les heures sans soleil, le drapeau se declenchait exactement quand le bilan sert. Une heure dont l'enveloppe demontree est nulle recoit desormais la qualite 'nuit'. Scenario epingle sur l'enveloppe et le taux de censure releves en production, pas sur le fixture — dont la nuit valait 0 et qui ne reproduisait donc pas le defaut. Preuve par mutation : avant 'minoree', apres 'nuit'. Suite 37 -> 38. Documente la nuit du 25-26/08 : profil corrige verifie a 0,2 kWh/h pres, bilan qui predit le point bas a un point pres, et non-linearite du SOC mesuree (huit points en dix-sept minutes pour 0,16 kWh).
This commit is contained in:
@@ -425,6 +425,151 @@ l'utilisateur. C'est voulu pour un câblage réel en période d'essai, mais il f
|
|||||||
le savoir — et le retirer le jour où le câblage sera adopté.
|
le savoir — et le retirer le jour où le câblage sera adopté.
|
||||||
|
|
||||||
|
|
||||||
|
## 26/08/2026 — La nuit qui valide le bilan, et le défaut du mètre
|
||||||
|
|
||||||
|
### Déroulé (véhicule débranché à 03:01, donc hors mécanique de nuit EVCS)
|
||||||
|
|
||||||
|
| Heure | Événement |
|
||||||
|
|---|---|
|
||||||
|
| 01:24:03 | Fenêtre HC → EDF ON, charge du véhicule dès 01:25:30 |
|
||||||
|
| 02:52:30 | Fin de charge détectée → `night_recovery_done_idle` |
|
||||||
|
| 02:53:03 | EDF coupé **3 secondes** plus tard |
|
||||||
|
| 03:01 | Débranchement |
|
||||||
|
| 05:10 | SOC affiché 20 % → EDF ON (branche « SOC bas » de l'hystérésis) |
|
||||||
|
| 06:18 | SOC affiché 30 % → EDF OFF |
|
||||||
|
| 09:06 | Point bas de la nuit : 23 % affichés, puis le PV prend le relais |
|
||||||
|
|
||||||
|
Réseau importé : 5,70 kWh pour le véhicule (01 h et 02 h, mesurés sur
|
||||||
|
`energy_from_ac_in_1_to_ac_out`) et 1,71 kWh pour la batterie (05:10-06:19,
|
||||||
|
intégration de `battery_power`). **Aucun yoyo** : une seule session, 69 minutes,
|
||||||
|
20 → 30 %. L'hystérésis a fait exactement ce pour quoi elle est écrite.
|
||||||
|
|
||||||
|
### Le profil de consommation corrigé colle à la mesure
|
||||||
|
|
||||||
|
Le correctif d'attribution horaire de la veille se vérifie sur une nuit
|
||||||
|
complète — profil publié contre consommation réellement mesurée :
|
||||||
|
|
||||||
|
| Heure | p50 publié | Mesuré | Heure | p50 publié | Mesuré |
|
||||||
|
|---|---:|---:|---|---:|---:|
|
||||||
|
| 21 h | 1,60 | 1,50 | 03 h | 0,70 | 0,80 |
|
||||||
|
| 22 h | 1,40 | 1,40 | 04 h | 0,70 | 0,70 |
|
||||||
|
| 23 h | 1,30 | 1,20 | 07 h | 0,70 | 0,70 |
|
||||||
|
| 00 h | 1,10 | 1,00 | 08 h | 0,80 | 0,70 |
|
||||||
|
|
||||||
|
Écart maximal 0,2 kWh/h. Le matin ne porte plus l'import réseau de la nuit
|
||||||
|
suivante.
|
||||||
|
|
||||||
|
### Le bilan a prédit le point bas de la nuit à un point près
|
||||||
|
|
||||||
|
`required_soc` publié heure par heure, contre le SOC réellement observé :
|
||||||
|
|
||||||
|
| Heure | Requis | Réel | Marge |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 21 h | 90,5 | 69 | −21,5 |
|
||||||
|
| 00 h | 62,7 | 45 | −17,7 |
|
||||||
|
| 03 h | 46,2 | 36 | −10,2 |
|
||||||
|
| 06 h | 32,1 | 29 | −3,1 |
|
||||||
|
| 07 h | 27,5 | 27 | −0,5 |
|
||||||
|
| 08 h | 23,3 | 24 | **+0,7** |
|
||||||
|
| 09 h | 22,0 | 23 | **+1,0** |
|
||||||
|
|
||||||
|
Le bilan annonçait le croisement à 09 h : le point bas est tombé à 09:06. Il
|
||||||
|
annonçait 22 % nécessaires à 09 h : la batterie a touché 23 %. C'est la
|
||||||
|
première validation quantitative du modèle sur une nuit entière.
|
||||||
|
|
||||||
|
Deux conséquences immédiates :
|
||||||
|
|
||||||
|
1. **Le seuil fixe a coupé une fois de trop tôt.** À 06:00 le bilan demandait
|
||||||
|
32,1 %, `grid_soc_off` a coupé à 30 %. La batterie est ensuite descendue à
|
||||||
|
23 %, soit **un point au-dessus du plancher dur**. Le seuil fixe n'était pas
|
||||||
|
grossièrement faux — il était court de 2 points, et c'est le lever du jour
|
||||||
|
qui a payé la différence. Une matinée couverte aurait traversé le plancher.
|
||||||
|
2. **La cible SOC du matin n'a joué aucun rôle cette nuit.** Le véhicule est
|
||||||
|
parti à 03:01, donc `nightSessionActive` est retombé à faux et toute la
|
||||||
|
mécanique de nuit EVCS avec lui. Ce sont `grid_soc_on`/`grid_soc_off`
|
||||||
|
(20/30, dans le workflow d'hystérésis) qui ont gouverné — et **le conseiller
|
||||||
|
IA ne peut pas les toucher**, même toggle activé.
|
||||||
|
|
||||||
|
### Le défaut du mètre : le SOC n'est pas linéaire dans la bande qui décide
|
||||||
|
|
||||||
|
Énergie réellement échangée par point de SOC, mesurée par intégration de
|
||||||
|
`sensor.victron_battery_power` :
|
||||||
|
|
||||||
|
| Fenêtre | ΔSOC | Énergie | kWh / point | Capacité implicite |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| 69 → 39 % (décharge) | 30 | 5,11 kWh | 0,170 | 17,0 kWh |
|
||||||
|
| **35 → 20 % (décharge)** | **15** | **1,07 kWh** | **0,071** | **7,1 kWh** |
|
||||||
|
| 20 → 30 % (charge) | 10 | 1,71 kWh | 0,171 | 17,1 kWh |
|
||||||
|
| 30 → 23 % (décharge) | 7 | 1,19 kWh | 0,170 | 17,0 kWh |
|
||||||
|
| 23 → 80 % (charge) | 57 | 9,06 kWh | 0,159 | 15,9 kWh |
|
||||||
|
|
||||||
|
Tout vaut 0,16-0,17 kWh par point — sauf la descente 35 → 20 %, qui n'en vaut
|
||||||
|
que 0,071. En regardant de plus près, la chute est concentrée : **de 04:17 à
|
||||||
|
04:34, le SOC affiché passe de 32 % à 24 %, huit points en dix-sept minutes**,
|
||||||
|
sous une décharge parfaitement stable de 560 W. Huit points « coûtent » 0,16 kWh
|
||||||
|
là où ils devraient en coûter 1,3.
|
||||||
|
|
||||||
|
La tension tranche : 48,81 V à 04:17, 48,75 V à 04:34, soit 60 mV pour huit
|
||||||
|
points, quand les six points précédents en avaient consommé 200. **Le pack n'a
|
||||||
|
pas perdu cette énergie ; c'est l'estimateur qui a décroché.** Comportement
|
||||||
|
classique d'un BMS Pylontech dont un module est plus faible que les autres — et
|
||||||
|
le parc en compte un mort isolé et un second qui faiblit (§0 du manuel).
|
||||||
|
|
||||||
|
Conséquence directe : la session réseau de 05:10 a été déclenchée par un « 20 % »
|
||||||
|
qui valait physiquement 26 à 28 %. Elle restait justifiée au fond — le bilan
|
||||||
|
demandait 36,9 % à 05:00 — mais elle a été **déclenchée par le mauvais
|
||||||
|
instrument, pour la bonne raison, par hasard**.
|
||||||
|
|
||||||
|
### Le drapeau « dégradé » criait au loup toutes les nuits
|
||||||
|
|
||||||
|
Introduit la veille, il a affiché `degrade` de 20:00 à 07:00. Cause : dans
|
||||||
|
`RESOURCE_ESTIMATE.js`, la qualité de forme d'une heure vient du taux de
|
||||||
|
censure, et les heures de nuit portent un taux de 1 — non pas parce que la
|
||||||
|
mesure serait douteuse, mais parce qu'il n'y a jamais eu de soleil à observer.
|
||||||
|
Or les heures de déficit *sont* les heures sans soleil : le drapeau se
|
||||||
|
déclenchait donc **toutes les nuits, c'est-à-dire exactement quand le bilan
|
||||||
|
sert**. Le commentaire du code disait déjà la bonne règle ; le code ne
|
||||||
|
l'appliquait pas.
|
||||||
|
|
||||||
|
Correctif : une heure dont l'enveloppe démontrée est nulle reçoit la qualité
|
||||||
|
`'nuit'` et non `'minoree'` — elle n'a aucune forme à minorer. Scénario
|
||||||
|
`ressource-nuit-forme-non-minoree`, épinglé sur l'enveloppe et le taux de
|
||||||
|
censure **relevés en production**, pas sur le fixture (dont la nuit valait 0 et
|
||||||
|
qui ne reproduisait donc pas le défaut — le piège du 24/08, à nouveau). Preuve
|
||||||
|
par mutation : code d'avant → `minoree`, après → `nuit`. Suite 37 → 38.
|
||||||
|
|
||||||
|
### Conclusion sur la cible SOC du matin
|
||||||
|
|
||||||
|
Trois arguments convergent, et ils déplacent la cible du chantier :
|
||||||
|
|
||||||
|
1. **La bonne grandeur n'est pas un SOC, c'est une énergie.** Le bilan calcule
|
||||||
|
déjà l'énergie à couvrir jusqu'au croisement, et il vient de le faire à un
|
||||||
|
point près sur douze heures. La cible de 35 % est une valeur de table ; la
|
||||||
|
valeur physique de cette nuit-là était 32,1 % à 06:00.
|
||||||
|
2. **Convertir une énergie en points de SOC réimporte le défaut du mètre.**
|
||||||
|
`usable_capacity_kwh = 15` appliqué linéairement suppose que tous les points
|
||||||
|
se valent ; les mesures ci-dessus montrent que ceux de la bande 24-32 %,
|
||||||
|
celle où se prennent toutes les décisions de nuit, peuvent ne rien valoir.
|
||||||
|
Tant que la conversion reste linéaire, câbler l'advisor sur la cible ne fait
|
||||||
|
que déplacer un nombre arbitraire vers un nombre calculé sur une règle
|
||||||
|
graduée fausse.
|
||||||
|
3. **La cible du matin n'est pas le seuil qui gouverne le plus souvent.** Elle
|
||||||
|
ne s'applique qu'avec un véhicule branché pendant la fenêtre HC. Les nuits
|
||||||
|
sans véhicule — celle-ci — sont gouvernées par `grid_soc_off`, que personne
|
||||||
|
n'arbitre et que le conseiller ne voit même pas.
|
||||||
|
|
||||||
|
Ordre de travail qui en découle, avant tout câblage :
|
||||||
|
|
||||||
|
- **a.** Rendre la conversion énergie ↔ SOC honnête : soit une courbe de
|
||||||
|
capacité par bande de SOC construite sur l'historique (la donnée existe, elle
|
||||||
|
vient d'être mesurée), soit un drapeau qui signale que le SOC vient de
|
||||||
|
décrocher et qu'il ne faut pas décider dessus pendant N minutes.
|
||||||
|
- **b.** Donner au bilan le seuil qui gouverne réellement : `grid_soc_off`,
|
||||||
|
côté hystérésis. C'est le chiffre qui a manqué de 2 points cette nuit, et
|
||||||
|
c'est le seul que le conseiller ne peut pas atteindre aujourd'hui.
|
||||||
|
- **c.** Ensuite seulement, la cible SOC du matin — qui deviendra un
|
||||||
|
sous-produit du bilan plutôt qu'un réglage indépendant.
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
- [ ] **Doublon `unique_id: victron_vebus_dc_power`** — déclaré deux fois, avec
|
- [ ] **Doublon `unique_id: victron_vebus_dc_power`** — déclaré deux fois, avec
|
||||||
|
|||||||
@@ -137,16 +137,27 @@ const dayKey = (ms) =>
|
|||||||
const currentHourStart = Math.floor(nowMs / 3600000) * 3600000;
|
const currentHourStart = Math.floor(nowMs / 3600000) * 3600000;
|
||||||
const today = dayKey(nowMs);
|
const today = dayKey(nowMs);
|
||||||
|
|
||||||
|
// Une heure ou l'enveloppe demontree est nulle est une heure de nuit : il n'y
|
||||||
|
// a jamais eu de soleil a y observer. Son taux de censure vaut 1 pour cette
|
||||||
|
// raison-la, pas parce que la mesure serait douteuse, et il n'existe aucune
|
||||||
|
// forme a minorer. La classer « minoree » faisait crier au loup TOUTES les
|
||||||
|
// nuits (les heures de deficit sont justement les heures sans soleil), donc
|
||||||
|
// exactement quand le bilan sert. Constate sur la nuit du 25-26/08 : statut
|
||||||
|
// « degrade » de 20 h a 07 h alors que le bilan a predit le point bas a un
|
||||||
|
// point pres.
|
||||||
const slots = [];
|
const slots = [];
|
||||||
for (let i = 0; i < CFG.HORIZON_HOURS; i++) {
|
for (let i = 0; i < CFG.HORIZON_HOURS; i++) {
|
||||||
const start = currentHourStart + i * 3600000;
|
const start = currentHourStart + i * 3600000;
|
||||||
const hour = hourOfDay(start);
|
const hour = hourOfDay(start);
|
||||||
|
const nightHour = !!envelope && !(envelope[hour] > 0);
|
||||||
slots.push({
|
slots.push({
|
||||||
startMs: start,
|
startMs: start,
|
||||||
hour,
|
hour,
|
||||||
day: dayKey(start),
|
day: dayKey(start),
|
||||||
weight: envelope ? envelope[hour] : 0,
|
weight: envelope ? envelope[hour] : 0,
|
||||||
quality: censorshipRate[hour] <= CFG.CENSORSHIP_QUALITY_LIMIT ? 'fiable' : 'minoree',
|
quality: nightHour
|
||||||
|
? 'nuit'
|
||||||
|
: censorshipRate[hour] <= CFG.CENSORSHIP_QUALITY_LIMIT ? 'fiable' : 'minoree',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ const SUMMARIZE = {
|
|||||||
todayExpected: o.todayExpectedRemainingKwh,
|
todayExpected: o.todayExpectedRemainingKwh,
|
||||||
tomorrowExpected: o.tomorrowExpectedKwh,
|
tomorrowExpected: o.tomorrowExpectedKwh,
|
||||||
shapeConfidenceBelow: o.source.shapeConfidence != null && o.source.shapeConfidence < 0.5,
|
shapeConfidenceBelow: o.source.shapeConfidence != null && o.source.shapeConfidence < 0.5,
|
||||||
|
// Qualite de forme d'une heure de nuit (03 h) et d'une heure de plein
|
||||||
|
// apres-midi (14 h) : sert au scenario de nuit, ou une heure sans soleil
|
||||||
|
// ne doit pas etre declaree « minoree ».
|
||||||
|
qualityAtHour3: (o.horizon.find(h => h.hour === 3) || {}).shapeQuality,
|
||||||
|
qualityAtHour14: (o.horizon.find(h => h.hour === 14) || {}).shapeQuality,
|
||||||
}),
|
}),
|
||||||
BALANCE: o => o.usable ? ({
|
BALANCE: o => o.usable ? ({
|
||||||
usable: true,
|
usable: true,
|
||||||
|
|||||||
@@ -159,6 +159,32 @@ export const resourceScenarios = [
|
|||||||
shapeConfidenceBelow: true,
|
shapeConfidenceBelow: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
node: 'RESOURCE_ESTIMATE',
|
||||||
|
id: 'ressource-nuit-forme-non-minoree',
|
||||||
|
label: 'Projection lancee en pleine nuit : une heure sans soleil n\'a aucune forme a minorer, elle ne doit pas degrader la confiance',
|
||||||
|
snapshot: {
|
||||||
|
// 22:30 : l'horizon couvre les heures 0 a 6, dont le taux de censure
|
||||||
|
// vaut 1 faute de soleil a observer, pas faute de mesure fiable.
|
||||||
|
nowMs: Date.parse('2026-08-25T22:30:00+02:00'),
|
||||||
|
forecast_today_remaining_kwh: '0',
|
||||||
|
forecast_tomorrow_kwh: '32',
|
||||||
|
// Enveloppe et taux de censure RELEVES EN PRODUCTION le 26/08/2026, pas
|
||||||
|
// ceux du fixture (capture plus ancienne ou la nuit valait 0). En
|
||||||
|
// production la nuit vaut 1 : c'est cette valeur-la qui declenchait le
|
||||||
|
// faux « degrade », et c'est donc elle qu'il faut tester.
|
||||||
|
envelope_kwh: [0,0,0,0,0,0,0,0.17,1.03,3.86,6.55,7.17,9.33,8.38,
|
||||||
|
7.78,6.37,3.77,4.78,2.55,2.72,0.91,0.16,0,0],
|
||||||
|
censorship_rate: [1,1,1,1,1,1,1,0,0,0,0,0,0.25,0.36,
|
||||||
|
0.44,0.63,0.71,0.88,0.80,0.75,0.08,0,1,1],
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
usable: true,
|
||||||
|
qualityAtHour3: 'nuit',
|
||||||
|
// L'apres-midi, lui, reste bien signale comme minore.
|
||||||
|
qualityAtHour14: 'minoree',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
node: 'RESOURCE_ESTIMATE',
|
node: 'RESOURCE_ESTIMATE',
|
||||||
id: 'ressource-matin-forme-fiable',
|
id: 'ressource-matin-forme-fiable',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user