# 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 pour le code** : l'enveloppe doit disparaître de `RESOURCE_ESTIMATE` au profit de la courbe horaire de l'API (`estimate/watthours/period`), disponible sur 6 jours. Elle apporte à la fois la forme et la magnitude, ce qui **simplifie** le nœud et supprime la référence défectueuse. L'enveloppe reste utile pour une seule chose : le plafond physique de vraisemblance. ## 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).