Files
EVCS/docs/CHANTIER_BILAN_ENERGETIQUE.md
Tallagrand Thierry 92f07fe885 Etude de la frontiere de l excedent : la voiture est un delestage, pas une charge
Integration de l historique HA sur 8 jours : 64% de l energie livree au
vehicule vient d un excedent reel, 16% sort de la reserve batterie. La
frontiere se ferme entre 18h30 et 19h50 fin aout, franchement.

Cote reserve nocturne, le bilan repond deja et il repond mal a notre
avantage : sept jours sur huit, meme pleine, la batterie ne passe pas la
nuit. Le deficit est structurel, ce qui est evitable c est de l aggraver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-09-01 00:49:13 +02:00

728 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Chantier — Bilan énergétique partagé
> Journal de bord du chantier ouvert le 24/08/2026. Documentation définitive à
> écrire à la fin ; ce fichier ne sert qu'à ne rien perdre en route.
## Pourquoi ce chantier
Constat du propriétaire, 24/08/2026 : « nous avons essayé de résoudre un défaut
de stratégie par une suite de solutions tactiques ». Trois oscillateurs
identifiés le même jour (règle de coupure EDF « Fix G », override d'abondance P6
qui détruisait sa propre condition d'autorisation, inversion des planchers de
démarrage et de maintien) — tous de la même nature : des seuils empilés par
couches successives, chacun défendable isolément, dont personne ne vérifiait la
cohérence d'ensemble. Chaque correctif créait l'oscillateur suivant.
**Toute valeur fixe est traitée comme une erreur de conception**, pas comme un
réglage à affiner.
## Décisions actées
| Sujet | Décision |
|---|---|
| Profil horaire | Recalculé une fois par jour, publié dans des helpers. Pas de recalcul dans la boucle 30 s. |
| Prudence | Percentile 20, appliqué **une seule fois sur le bilan net** (PV − conso par heure et par jour), jamais séparément sur chaque terme — sinon on cumule deux pessimismes et le déficit passe à 109 % de la batterie, système inutilisable. |
| SOC | Lissé pour les décisions de pilotage, valeur instantanée conservée pour les planchers de sécurité. |
| Consommation | Mesurée, jamais supposée. Filtrage contextuel. Mesure non biaisée. |
| Production PV | **Jamais la mesure brute.** Sans injection, `PV_mesurée = min(ressource, conso + absorption batterie)` : observation censurée dès que la batterie est pleine. Estimateur = prévision, recalée uniquement sur les heures non censurées. |
| Rétention historique | `recorder.purge_keep_days = 90` (appliqué le 24/08, redémarrage HA fait). Les statistiques long terme étaient déjà sans limite. |
| Conseiller IA | Les faits restent arithmétiques et déterministes ; l'advisor traite les hypothèses (facteur de confiance sur la prévision, lecture des états et alarmes, explication). Montée en pouvoir en trois paliers, sur un score de prédiction vérifié contre la mesure. |
## Sorties attendues du bilan
Heure de croisement PV/conso · énergie à couvrir d'ici là · niveau de batterie
requis (remplace `grid_soc_off` fixe) · marge disponible pour le véhicule ·
drapeau « production actuellement bridée » · drapeau de santé des mesures.
**Objectif ajouté le 24/08 : dimensionner le stockage.** Le propriétaire vise
l'autonomie et veut savoir combien de batteries ajouter. La ressource gaspillée
par la censure est précisément l'énergie qu'un stockage supplémentaire
capterait : le bilan doit donc produire, en plus, une estimation de l'énergie
perdue par manque de puits et le gain attendu par kWh de batterie ajouté.
Cela impose une simulation multi-jours sur la ressource *décensurée* — c'est
l'étape 3 qui la rend possible.
### Constat mesuré sur 27 jours (28/07 → 24/08)
| Grandeur | Moyenne / jour |
|---|---|
| Production PV (censurée, donc minorée) | 38,7 kWh |
| Consommation totale | 43,1 kWh (dont véhicule 10,0) |
| Décharge batterie | 11,5 kWh (p90 14,3 · max 16,1 sur 17,5 utiles) |
| **Import réseau** | **7,83 kWh** — un seul jour sur 27 à zéro |
La batterie se vide presque intégralement chaque jour : c'est elle le facteur
limitant, pas la puissance des onduleurs.
### Parc batterie réel (précisé le 24/08)
8 Pylontech US2000C **en deux packs de 4 avec un hub**, le premier pack ayant
deux ans de plus que le second. Un module est en panne et isolé : **le système
tourne actuellement sur 7**, et un huitième commence à faiblir. Le vieux pack a
vécu avant la climatisation du garage. Mélanger des packs d'âges différents est
déconseillé — contrainte de budget, pas choix technique.
Conséquence directe sur le code : `batteryUsableCapacityKwh: 17.5` est codé en
dur dans `INPUT_PARSE.js` et utilisé trois fois dans `WINDOWS_SOURCEV2.js`
(tampon de prévision, SOC projeté en fin de journée, cible du matin). Avec 7
modules dont un fatigué, la capacité réelle est plutôt de l'ordre de 14-15 kWh :
**le contrôleur surestime sa réserve d'environ 15 à 20 %.** Aucun capteur de
capacité ou de santé batterie n'existe côté HA — à publier depuis le BMS pour
que cette valeur cesse d'être une constante.
### Énergie perdue faute de puits — et pourquoi le recalage de la prévision échoue
Première estimation, avec un facteur de recalage global : 23,9 kWh/jour, 38 % de
la ressource. **Chiffre faux.** Le facteur global valait 1,22 mais il est
dominé par les heures de 8-9 h, où le recalage ressort à 4 voire 7 : la
prévision sous-estime massivement la rampe du matin. Or ces heures sont
sur-représentées parmi les heures exploitables, puisque la batterie y est
toujours vide.
Le problème est structurel : **la censure est corrélée au beau temps.** Les
heures exploitables de l'après-midi ne le sont que les jours médiocres — d'où
un recalage qui tombe à 0,24 l'après-midi, avec 1 à 5 échantillons. Les données
manquantes ne le sont pas au hasard, donc aucun facteur de recalage appris sur
cet échantillon n'est transportable. Cela invalide le plan initial de l'étape 3.
Estimation retenue à la place, **borne basse** : pour chaque heure censurée,
potentiel = min(prévision brute, enveloppe horaire réellement démontrée). Ne
suppose aucun recalage et ne dépasse jamais ce que l'installation a déjà produit
à cette heure-là.
| Perte par manque de puits (borne basse) | |
|---|---|
| Moyenne | **11,8 kWh/jour** (23 % de la ressource) |
| Médiane · p80 · max | 10,8 · 19,5 · 25,6 kWh |
| Jours sans perte | 2 sur 27 |
**Le résultat qui tranche la question du dimensionnement : on jette plus (11,8)
qu'on n'importe (7,83).** L'énergie existe déjà, il manque le réservoir et la
capacité à l'encaisser. Un ajout d'environ **+8 kWh utiles** est donc à la fois
nécessaire (côté demande) et remplissable (côté ressource). Réserve : chiffres
d'août ; l'historique de prévision remonte au 2 mars et permettra de refaire le
calcul en saison basse.
## Avancement
- [x] **Étape 1 — détection de la censure** (24/08). `src/energy-balance/` :
snapshot HA de 26 champs incluant les signaux jamais lus (états MPPT, état
VE.Bus, alarme de surcharge, ESS BatteryLife, état du bus DC, statut natif de
la borne, prévision horaire) et nœud `CENSORSHIP.js` qui qualifie chaque
échantillon. Suite `tests/energy-balance/` : 10 scénarios, dont un snapshot
réel du 24/08 14:29 correctement classé censuré. Mesure sur l'historique :
**48 % des heures de jour sont censurées, 77 à 96 % à partir de 13 h.**
- [x] **Étape 2 — profil de consommation mesuré** (24/08).
`src/energy-balance/CONSUMPTION_PROFILE.js` : compteurs d'énergie cumulés →
deltas horaires → distribution par heure de la journée → percentiles.
Validé sur 28 jours réels (639 heures, aucun compteur remis à zéro) :
consommation typique 31,9 kWh/jour hors véhicule, 42,8 kWh au p80. Le profil
tient dans un helper `input_text` (119 caractères sur 255).
Vérification préalable de la méthode : **seulement 7,8 % des heures de déficit
sont censurées** — censure et déficit sont physiquement presque exclusifs
(une heure censurée est une heure où la batterie n'avait plus de place, donc
une heure de surplus). La grandeur qui pilote la décision EDF est donc
mesurée sur des données propres.
- [x] **Étape 3 — estimateur de ressource PV** (24/08).
`src/energy-balance/RESOURCE_ESTIMATE.js`. **Deux approches essayées et
écartées, documentées dans l'en-tête du nœud** :
1. *Recalage de la prévision* — impossible, la censure est corrélée au beau
temps (facteur 4,29 à 9 h sur 27 échantillons, 0,48 à 15 h sur 3).
2. *Forme issue de la géométrie solaire* — un plan unique ajusté sur les
heures propres converge sur « plein est, 20° » et prédit 2,0 % de la
journée à 18 h contre 5,8 % mesurés. Le champ a plusieurs orientations
(trois trackers) qu'un plan unique ne représente pas.
Retenu : **forme = enveloppe horaire démontrée** (comportement réel du champ,
orientations multiples comprises, sans modèle) × **magnitude = prévision
Forecast.Solar** (météo, insensible à l'état de notre batterie). Le nœud
publie un indice de confiance par heure : l'enveloppe de l'après-midi étant
elle-même censurée, elle est signalée « minorée » au lieu d'être présentée
comme fiable. Sans enveloppe fournie, le nœud se déclare inutilisable plutôt
que d'inventer une forme. Suite portée à 17 scénarios.
- [ ] **DÉFAUT TROUVÉ — orientation Forecast.Solar mal déclarée.** Forme prévue
contre forme mesurée, en part de la journée : matin 07-11 h **15,1 % prévus
contre 39,3 % réels** ; après-midi 14-18 h **52,1 % prévus contre 35,2 %**.
La prévision attend une journée orientée l'après-midi, l'installation produit
le matin. Les totaux journaliers restent à peu près justes, mais la
répartition intra-journalière est inutilisable — ce qui dégrade toutes les
décisions fondées sur la prévision (EVCS, chauffe-eau, conseiller IA).
À corriger dans la configuration de l'intégration (azimut et inclinaison par
plan), puis refaire la comparaison.
- [x] **Étape 4 — le bilan** (24/08). `src/energy-balance/BALANCE.js`. Ne pilote
rien : publie les faits qui remplacent les seuils fixes — heure de croisement,
énergie à couvrir, **SOC requis** (remplaçant de `grid_soc_off`), énergie
réseau strictement nécessaire, surplus disponible pour le véhicule, indice de
confiance. Sans une de ses entrées, il se déclare inutilisable et les
contrôleurs gardent leurs règles.
**Deux niveaux de prudence, et c'est un choix de conception, pas un détail.**
Le premier jet n'utilisait que le profil prudent (p80) et exigeait **48 % de
SOC** pour franchir la matinée du 24/08 — alors que le seuil fixe de 30 %
aurait été moins coûteux. Un bilan plus gourmand que le seuil bête qu'il
remplace n'a aucun intérêt. Corrigé : la décision réseau, réévaluée à chaque
cycle et réversible, utilise le profil **typique (p50)** ; le profil prudent
(p80) sert à la planification qui engage plusieurs heures (cible de nuit,
dimensionnement du stockage). Se tromper par optimisme ne coûte qu'un
démarrage réseau une heure plus tard, le plancher dur protégeant la batterie ;
se tromper par excès de prudence coûte un import réseau immédiat et réel.
Vérifié aussi : la marge de sécurité de 10 % que j'avais ajoutée était une
constante arbitraire de plus, et un double comptage. Mesure du 24/08 :
`p80(somme des heures)` égale `somme(p80 par heure)` sur toutes les fenêtres
testées — les heures de consommation sont fortement corrélées. La prudence est
donc entièrement portée par le percentile, qui est mesuré. Marge supprimée.
**Rejeu de la matinée du 24/08 (scénario figé)** — SOC réel 21 %, capacité
15 kWh, production horaire réellement mesurée :
| | |
|---|---|
| Croisement PV/consommation | **08 h** |
| Énergie à couvrir | **0,82 kWh** (calcul manuel sur les mesures : 0,80) |
| SOC requis (décision) | **27,5 %** au lieu du seuil fixe de 30 % |
| Réseau nécessaire | oui, **0,97 kWh** — une session, puis terminé |
| SOC requis (planification, p80) | 38,1 % |
| Déficit calculé sur forme fiable | oui |
Suite portée à **21 scénarios**.
- [x] **Étape 5 — publication en mode ombre** (24/08). Workflow n8n
**EVCS Energy Balance V1** (`QoSfPeyGq35orF1K`), actif, cycle 5 min.
Périmètre d'écriture garanti par test de mutation : l'injection d'un
`turn_on` sur `switch.edf_general_ok_switch` fait rougir la suite.
Kill-switch `input_boolean.evcs_balance_enabled`.
- [x] **Étape 5b — recalcul quotidien des profils** (24/08). Workflow
**EVCS Energy Profiles Daily V1** (`N6UaB4ZozsbRPrFb`), actif, 03:30.
Lit `/api/history/period` et republie les quatre CSV.
⚠️ Piège trouvé : **sans `end_time`, l'API ne renvoie qu'UNE journée**, en
silence. Mes trois premières mesures « sur 3 jours » n'en couvraient qu'une.
Refus explicite d'écraser les profils si l'historique ne couvre pas les 24 h.
**Validation croisée** : profils calculés depuis l'API REST (états bruts,
11 jours) contre profils calculés depuis les statistiques long terme
(28 jours) → **0,20 kWh/h d'écart absolu moyen**, deux chemins de données
totalement indépendants.
- [x] **Étape 5c — prochaine fenêtre de déficit** (24/08). Le bilan ne
répondait qu'à « combien jusqu'à ce que le solaire prenne le relais » : à
15 h la réponse est zéro, exacte mais inutile, car le déficit qui approche
est celui de la nuit. Ajout de la prochaine traversée déficitaire — heure de
début, énergie, SOC à atteindre avant. **Ce SOC n'est volontairement pas
écrêté à 100 %** : au-delà, même une batterie pleine ne passe pas, et l'écart
est publié comme `storage_shortfall_kwh`. C'est le chiffre du dimensionnement
du stockage.
- [x] **Étape 6 — tableau de bord** (24/08). Dashboard YAML dédié
`bilan-energetique` (icône balance, barre latérale), copie versionnée dans
`dashboards/bilan_energetique.yaml` du dépôt.
Vue **Bilan** : la comparaison seuil calculé / seuil fixe / SOC réel avec son
commentaire automatique, la décision immédiate, la prochaine traversée
déficitaire, une alerte conditionnelle quand le stockage est insuffisant,
l'état de bridage de la production, et deux graphiques d'historique
(SOC contre seuils, bridage contre réseau EDF).
Vue **Profils & réglages** : le tableau horaire des quatre profils avec le
taux de censure par heure, la capacité batterie (modifiable), le kill-switch.
Déclaré dans `configuration.yaml` (sauvegarde dans `SAVE_CONFIG/`),
redémarrage HA effectué, aucune erreur au journal.
## Modèle solaire local : tentative abandonnée (25/08)
Objectif : produire la forme horaire à partir de la géométrie et des quatre
orientations réelles, pour éviter un abonnement Forecast.Solar. **Abandonné**,
pour trois raisons qui se cumulent.
**Découverte utile au passage** : les entrées MPPT sont mesurées séparément et
la correspondance est établie — `sensor.airtria_mppt1_yield_today` = les 10 PV
sud-est (pic à 11 h), `sensor.v1_yield_today` = les 3 PV du suiveur (pic à 9 h),
`tracker_solar_1/2/3` du RS 450/200 = les trois strings sud (pics 13-14 h),
`tracker_solar_4` = le string ouest (longue traîne jusqu'à 19 h). Chaque plan
peut donc être confronté à sa propre mesure — c'est ce qui a permis de trancher.
1. **Seules 4 heures de la journée sont exploitables.** Taux de censure :
08 h 0 %, 09 h 0 %, 10 h 4 %, 11 h 4 %, puis **12 h 59 %, 13 h 77 %,
14 h 81 %**. Ajuster une forme journalière sur quatre heures propres n'est
pas sérieux.
2. **L'enveloppe n'est pas une courbe de journée valide.** C'est le maximum
horaire pris indépendamment sur des jours différents : le pic de 8 h vient
d'un jour, celui de 11 h d'un autre. Le résultat est plus plat que n'importe
quelle journée réelle. Comparer un modèle de journée à cette référence n'a
pas de sens. *(Limitation qui vaut aussi, au second ordre, pour l'usage de
l'enveloppe comme forme dans `RESOURCE_ESTIMATE` — atténuée par le fait
qu'elle y est renormalisée sur la prévision du jour.)*
3. **Le modèle a besoin de paramètres non ajustables sur ces données.** Un
modèle direct seul prédit zéro production le matin sur le plan ouest, alors
que la mesure donne 14 % à 9 h : il manquait le **rayonnement diffus**.
Ajouté, sa fraction part en butée de l'intervalle de recherche sans jamais
converger — le modèle reste trop piqué sur midi par rapport à la mesure, et
ce pour les quatre plans à la fois.
**Conséquence** : l'abonnement Forecast.Solar Professional (souscrit le 24/08,
70 €/an, 14 jours d'essai) est le bon choix, et pour une raison que je n'avais
pas identifiée en le déconseillant. Avec la clé d'API, n8n interrogera
directement `api.forecast.solar` et obtiendra la **courbe horaire complète**
(`watt_hours_period`) pour les trois plans — ce que l'intégration HA n'expose
pas, même abonnée, puisqu'elle ne publie que quatre capteurs résumés.
Cette courbe remplacera **à la fois** la forme (enveloppe) et la magnitude
(capteurs) dans `RESOURCE_ESTIMATE`, ce qui simplifiera le nœud au lieu de le
compliquer. Les plans à déclarer : sud-est 135°/40°/4030 W (suiveur fusionné,
930 W), sud 180°/35°/9300 W, ouest 270°/45°/2790 W.
## Validation des trois plans (25/08) — et correction d'un chiffre que j'ai trop cité
Configuration appliquée : trois plans (sud-est+suiveur 135°/40°/4030 W,
sud 180°/35°/9300 W, ouest 270°/45°/2790 W), `inverter_size` 8000 → 11000,
clé d'API Professional en place. La prévision du jour passe de **30,2 à
43,5 kWh**. Aucune erreur au journal.
⚠️ **L'API doit être appelée avec sa propre convention d'azimut** : 0 = sud,
−90 = est, +90 = ouest, soit `azimut_API = azimut_HA − 180`. Et les plans
multiples s'écrivent en triplets successifs dans le chemin
(`/dec1/az1/kwp1/dec2/az2/kwp2/...`), pas en listes séparées par des virgules —
celles-ci renvoient une erreur 422.
**Résultat du test.** Forme ciel clair contre enveloppe mesurée :
matin 07-11 h **21,4 %** contre 39,3 % mesurés (c'était 15,1 % avant
correction) ; après-midi 14-18 h 50,6 % contre 35,2 %. L'écart absolu moyen
tombe de ~4,5 à **2,53 points**. Amélioration nette, mais l'écart ne se referme
pas.
**Et c'est ma référence qui est en cause.** Confrontation plan par plan, chacun
contre son propre MPPT, sur les 4 heures à censure ≤ 4 % : le modèle ciel clair
de Forecast.Solar sous-estime 8 h et surestime 11 h — **exactement le même biais,
et de même ampleur, que mon modèle géométrique local**. Deux modèles
indépendants s'accordent entre eux et divergent de l'enveloppe : le plus
probable est que l'enveloppe ait tort.
Mécanisme : l'enveloppe est un maximum horaire pris sur des jours différents. Le
rapport max/moyenne est plus grand le matin que le midi (une matinée claire et
une matinée de brume diffèrent bien plus que deux midis), donc l'enveloppe
**surpondère structurellement le matin**.
**Correction assumée** : le « 39,3 % du matin mesurés » que j'ai cité toute la
journée comme preuve du défaut de configuration était lui-même gonflé. Le défaut
était réel — un seul plan plein sud, 15 kWp, plafonné à 8 kW — mais j'en ai
surestimé l'ampleur en le mesurant contre une référence biaisée.
**Conséquence appliquée le 25/08** : `RESOURCE_ESTIMATE` lit désormais la courbe
horaire de l'API, publiée par le workflow **EVCS Solar Forecast V1**
(`07P8I349ugna9FH4`, 15 min). Elle apporte forme et magnitude d'un seul tenant,
sur 6 jours, avec les trois plans réels, sans aucune censure — le nœud s'en
trouve simplifié : plus de répartition au prorata, plus de renormalisation.
L'enveloppe est **rétrogradée en repli** : elle sert encore si la courbe est
absente ou vieille de plus d'une heure, et le champ `mode` dit laquelle des deux
a servi (`courbe_horaire_api` ou `repli_enveloppe`). Elle garde par ailleurs un
rôle de repère de vraisemblance, **sans écrêtage** : étant minorée l'après-midi,
elle tronquerait à tort une prévision correcte.
## 25/08/2026 — Première nuit observée, et le défaut qu'elle a révélé
### Ce que la nuit a fait
| Heure | Événement |
|---|---|
| 01:24 | Fenêtre HC : EDF ON, charge du véhicule (P7) |
| 02:42 | Fin de charge détectée → `night_recovery_done_idle`, EDF coupé en 3 s |
| 02:44 → 02:45 | Véhicule débranché puis rebranché (93 s) |
| 02:47 → 04:25 | Seconde charge, 98 min ; puis EDF coupé |
| 06:22 → 06:49 | **EDF rallumé**, batterie 33 → 35 %, coupé |
| 07:02 → 07:27 | **EDF rallumé**, batterie 33 → 35 %, coupé |
Import réseau : **12,30 kWh**, dont 11,11 kWh entre 01 h et 04 h pour les
véhicules (l'EVCS en a pris 9,57) et **1,19 kWh entre 06 h et 07 h sans aucun
véhicule en charge**.
Les mécanismes de fin de charge ont fonctionné : détection franche à 02:42,
coupure immédiate, et P11 a correctement remis à zéro la mémoire « plein » au
rebranchement de 02:45 — le second véhicule a bien été rechargé.
### Le quatrième oscillateur
Les deux cycles de 06:22 et 07:02 sont un oscillateur de plus, de la même
famille que les trois du 24/08. La cible du matin
(`batteryMorningTargetPct` = 35 %) libère le takeover à `SOC > cible` et le
réarme à `SOC ≤ cible − 2`. Entre 33 et 35 %, avec une maison qui tire ~0,7 kWh/h
et une production encore nulle, la batterie traverse la bande en une vingtaine
de minutes : période mesurée ~35 min, deux cycles complets.
Ce n'est **pas** le seuil `grid_soc_off` de l'hystérésis (30 %) : à 33 % il
n'était pas en cause. C'est la cible du matin de l'EVCS. Et 35 % est une valeur
de table (règle « prévision forte → 35 »), pas un besoin calculé — le bilan,
une fois corrigé, demandait **31 %**.
### Le défaut de mesure que cette nuit a mis au jour
Le bilan annonçait à 06 h qu'il fallait **3,37 kWh** pour tenir jusqu'au
croisement. La consommation réelle sur cette tranche a été de 2,2 kWh, la
production de 0,85 : **1,35 kWh**. Deux fois et demie trop.
L'erreur ne venait pas du modèle mais de l'attribution horaire.
`bucketHourly` garde la dernière valeur de chaque heure ; le delta entre H−1 et
H est donc l'énergie dépensée **pendant H**. `deltaSeries` le rangeait sous
H−1.
Sur un compteur qui émet en continu, cela décale le profil d'une heure — gênant,
pas grave. Mais **les compteurs de cette installation sont épars** :
`ac_in_1_to_ac_out` ne peuple que **57 heures sur 267**, `evcs_total` 57 sur 264,
parce qu'ils n'émettent que lorsqu'ils avancent. Une heure vide veut dire
« valeur inchangée », donc zéro énergie : toute l'énergie d'un intervalle à trou
a été consommée pendant l'heure **finale**, celle qui a réveillé le compteur.
Rangée sous l'heure initiale, elle atterrissait n'importe où.
Mesuré : `consumption_p50[8] = 2,90 kWh/h` pour 0,80 réellement consommé. Six
échantillons sur douze portaient un delta étalé sur **17 heures** — l'import
réseau de la nuit suivante, déposé sur la dernière heure de la nuit précédente.
Le profil du matin était ainsi gonflé de l'import qu'il servait justement à
éviter : le bilan réclamait du réseau parce qu'il avait pris du réseau la veille.
Corrigé : le delta se range sous `hours[i]`. Profil du matin après correction —
0,7 kWh/h de 01 h à 07 h, 0,8 à 08 h — conforme aux 0,74 kWh/h mesurés ce
matin-là. Avec ce profil, le bilan aurait demandé **31 % à 06 h** contre 34 %
réels : pas de réseau. Sans les 1,19 kWh importés, la batterie serait descendue
à ~24 %, au-dessus du plancher dur de 22 % — mais de 2 points seulement.
**Troisième correction du même chiffre.** L'enveloppe de production subissait le
même décalage : la part matinale de la production était donc surestimée une
troisième fois, après le plafond `inverter_size` et après le biais max/moyenne
de l'enveloppe. Le pic de l'enveloppe passe de 11 h à 12 h. Aucune conclusion du
chantier ne change, mais le chiffre « 39,3 % du matin » doit être considéré comme
définitivement abandonné.
### Statut `degrade`
La nuit précédant la mise en service de la courbe horaire, le bilan a publié un
SOC requis de **100 %** avec un statut `ok`, faute de croisement trouvé sur
l'horizon. Il publie désormais `degrade` dans ce cas et quand les heures de
déficit reposent sur une forme minorée : les chiffres restent visibles, mais
plus présentés comme fiables. Un futur câblage ne pourra pas s'y fier par
inadvertance.
### Ce que la prévision, elle, a très bien fait
Courbe horaire Forecast.Solar contre production mesurée, ce matin :
| Heure | Prévu | Mesuré |
|---|---:|---:|
| 07 h | 0,08 kWh | 0,10 kWh |
| 08 h | 0,85 kWh | 0,75 kWh |
| 09 h | 3,07 kWh | 2,72 kWh |
Les trois plans réels et l'abonnement Professional donnent une ressource
utilisable telle quelle. **Le côté production du bilan est fiable ; c'était le
côté consommation qui mentait.**
### Suite
Le candidat évident au prochain câblage réel n'est plus l'hystérésis mais la
**cible SOC du matin** : c'est elle qui a coûté 1,19 kWh cette nuit, et le
toggle qui permettrait de la piloter existe déjà
(`input_boolean.evcs_advisor_apply_night_target`). À décider après quelques
nuits d'observation du seuil calculé contre le seuil de table.
⚠️ Ce toggle porte `initial: off` dans son package : **tout redémarrage de Home
Assistant le repositionne à OFF**, quelle que soit la valeur choisie par
l'utilisateur. C'est voulu pour un câblage réel en période d'essai, mais il faut
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
- [ ] **Doublon `unique_id: victron_vebus_dc_power`** — déclaré deux fois, avec
deux topics différents : `configuration.yaml:99`
(`system/0/Dc/Vebus/Power`) et `packages/victron_vebus_mqtt.yaml:63`
(`vebus/276/Dc/0/Power`). Vérifié le 24/08 : **les deux publient la même
grandeur** — un système VE.Bus en parallèle n'expose qu'une seule instance
D-Bus couvrant les deux onduleurs. Ce n'est donc pas un défaut de données,
seulement une ligne `ERROR` à chaque démarrage et un fichier qui ment sur ce
qu'il fait. Supprimer les lignes 62-68 du package, garder celle de
`configuration.yaml` (nom conforme à l'`entity_id` utilisé par `INPUT_PARSE`),
puis `mqtt.reload` — pas besoin de redémarrer.
- [ ] **`evcsDedicatedMaxW: 4900`** codé en dur dans `INPUT_PARSE.js`, appliqué
dès qu'EDF est absent (plafond ~20 A). Encore une valeur au jugé : à
confronter à la réalité du câblage du circuit dédié avant de la garder ou de
la remplacer par un helper.
- [ ] **Alarmes jamais lues** : `sensor.airtria_mpii_v1_overload_alarm`, alarme
« High charge current » du Battery Monitor (observée active le 24/08),
`sensor.gx_device_ess_batterylife_state` (peut imposer sa propre limite de
décharge à l'insu du contrôleur), `sensor.evcs_hq2239dnt7p_status`.
- [ ] **États MPPT jamais lus** (`sensor.airtria_mppt1_state`,
`sensor.mppt_450_200_hq2449yr9pt_state`) — indispensables : ils disent si un
échantillon de production est exploitable ou censuré.
- [ ] **Contrôle de vraisemblance `PV + VE.Bus = batterie`** — vérifié à 95 W
près le 24/08 (~9 %). À intégrer comme drapeau de santé permanent : le jour
où l'identité diverge, un capteur ment.
- [ ] **États zombies** `stop_source_lost` / `idle_wait_source` — noms
transitoires peu lisibles dans le journal et le tableau de bord (cf. HANDOFF
§24, hors périmètre à l'époque).
## 01/09/2026 — La frontière de l'excédent, mesurée sur 8 jours
Question du propriétaire : faut-il charger la voiture le soir ? Avec la règle
qu'il pose et qui gouverne désormais le sujet — **la voiture est un délestage de
l'excédent PV, pas une charge prioritaire qui vide les batteries solaires**, et
la frontière se détecte à la **puissance de décharge batterie**, pas à la
production PV ni à un seuil de SOC.
### Méthode
`tools/etude-frontiere-excedent.mjs` intègre l'historique HA (résolution native
~2 s, rétention 90 jours effective depuis le 24/08 — vérifié) et répartit
l'énergie **réellement livrée au véhicule**, instant par instant, entre trois
origines physiques :
```
batterie : min(P_evcs, décharge batterie) -> réserve entamée
réseau : min(reste, import réseau) -> énergie payante
excédent : le solde -> production qui aurait été
refusée ou simplement stockée
```
Aucune écriture, aucune décision : le script lit et intègre. Convention
d'attribution assumée : sans la voiture, la décharge batterie aurait été
réduite d'au plus `P_evcs` — l'attribution est donc une borne haute honnête de
ce que la voiture a coûté à la réserve.
### Ce que les 8 jours disent (24/08 → 31/08)
| | kWh | part |
|---|---:|---:|
| Livré au véhicule | 80,13 | 100 % |
| dont **excédent** | 51,18 | **64 %** |
| dont **batterie** | 13,22 | **16 %** |
| dont réseau (HC nuit, voulu) | 15,73 | 20 % |
Part d'excédent par heure, cumul sur la période :
| h | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | **19** | **20** |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| part excédent | 94 % | 99 % | 86 % | 80 % | 83 % | 90 % | 77 % | 92 % | 81 % | **38 %** | **4 %** |
**La frontière existe, elle est franche, et elle tombe entre 18 h 30 et 19 h 50**
(heure du dernier instant où le véhicule chargeait sans faire décharger la
batterie) :
| jour | fermeture | SOC max du jour |
|---|---|---:|
| 24/08 | 19:26 | 95 % |
| 25/08 | 19:29 | 95 % |
| 28/08 | 19:50 | 93 % |
| 29/08 | 18:47 | 94 % |
| 30/08 | 18:31 | 94 % |
| 31/08 | 20:06 (0 % d'excédent) | 95 % |
### Pourquoi la charge de journée, elle, est gratuite
La batterie **sature à 93-96 % tous les jours**. L'énergie détournée vers la
voiture entre 10 h et 18 h n'a donc coûté aucune réserve : le plateau était
atteint de toute façon, et la production au-delà était refusée (§ censure). Ce
n'est vrai que *tant que la batterie sature* — c'est le second terme de la
règle, et c'est lui qui bougera avec la saison.
### Le coût réel de la charge du soir
13,22 kWh pris à la réserve sur 8 jours, soit **1,65 kWh/jour**, rachetés au
réseau la nuit suivante — avec en plus le rendement aller-retour de la batterie
(~10 %) et un cycle de charge/décharge pour rien. Le tarif est le même (HC dans
les deux cas) : le surcoût est purement en pertes et en usure. Autrement dit,
charger le soir sur la batterie **revient à charger la voiture au réseau, en
plus cher et en abîmant le parc**.
### La deuxième question : combien garder pour la nuit ?
Elle est déjà résolue, et pas par un seuil. Le bilan publie
`evcs_balance_next_deficit_kwh` et `..._required_soc` — l'énergie de la
prochaine traversée déficitaire et le niveau à atteindre avant. Validation du
26/08 : point bas de la nuit prédit **à un point de SOC près sur douze heures**.
Mesure à 18 h, SOC requis pour franchir la nuit **sans réseau** :
| 24/08 | 25/08 | 26/08 | 27/08 | 28/08 | 29/08 | 30/08 | 31/08 |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 113,7 % | 119,4 % | 22,3 % ⚠️ | 114,3 % | 112,9 % | 112,3 % | 109,5 % | 117,9 % |
**Sept jours sur huit, même pleine, la batterie ne passe pas la nuit.** Déficit
structurel de 10 à 20 points, soit 1,5 à 3 kWh — c'est `storage_shortfall_kwh`,
le chiffre du dimensionnement, et il concorde avec les 11,8 kWh/jour jetés faute
de puits. L'import de nuit n'est donc pas évitable aujourd'hui ; ce qui est
évitable, c'est de l'**aggraver** en vidant la réserve le soir.
⚠️ Le 22,3 % du 26/08 est à vérifier (statut du bilan ce jour-là) avant d'être
cité.
### La règle qui en découle — deux termes, tous deux mesurés
```
charger le véhicule <=> (a) la batterie ne se décharge pas pour le nourrir
ET (b) elle atteindra quand même le niveau exigé par la
prochaine traversée déficitaire
```
- **(a)** est instantané, physique, déjà mesuré (`battery_power`), et se pilote
en asservissement : monter le courant tant que la batterie ne décharge pas,
redescendre dès qu'elle décharge. Ce n'est pas un seuil, c'est un servo.
- **(b)** est le bilan, déjà calculé toutes les 5 minutes
(`nextDeficit.reachable`). C'est lui qui portera la saison : quand les jours
raccourciront, la batterie cessera de saturer et (b) se fermera de lui-même,
de plus en plus tôt — sans qu'aucune constante n'ait à être touchée.
Ce couple remplace `batteryAssistSolarStartOk` (SOC ≥ 90), `soc_start_min`,
`midday_soc_min` et les planchers de profil : tous sont des mandataires de « la
production est refusée », justes à midi et faux au crépuscule.
### L'étude longue : rien à instrumenter
Constat utile : **l'historique HA suffit**. 2 s de résolution sur les puissances,
90 jours de rétention, et le bilan qui publie ses helpers toutes les 5 minutes
depuis le 24/08. La grandeur à suivre semaine après semaine est l'**heure de
fermeture de la frontière** et le **SOC max du jour** ; 8 jours ne montrent
aucune dérive saisonnière exploitable — il en faut plusieurs semaines, ce qui
est précisément l'objet de l'étude.
Bénéfice second, non cherché : les mêmes relevés donnent la courbe
énergie ↔ point de SOC par bande, c'est-à-dire le point (a) de l'ordre de
travail du 26/08, qui bloque tout câblage de la cible du matin.