Files
EVCS/dashboards/bilan_energetique.yaml
Tallagrand Thierry 49a6ee88db La prevision affichee cesse d etre de l histoire
Le graphe « production prevue et realisee heure par heure » du troisieme
onglet du bilan energetique ne montrait que du passe : sa courbe « prevue »
etait la prevision publiee le matin de chaque jour ECOULE, et la fenetre
s arretait a l heure en cours. C est un instrument de mesure du biais, pas
une prevision.

Deux courbes desormais, et elles n ont ni la meme provenance ni le meme
usage. Pour un jour ecoule, ce qui avait ete annonce LE MATIN MEME reste la
seule reference honnete : une prevision revisee a 18 h connait deja la
journee, la comparer au reel ne mesure plus rien. Pour les heures a venir,
c est la DERNIERE publication qui fait foi — aujourd hui jusqu au soir,
demain en entier. Les deux se touchent sur l heure en cours au lieu de
laisser un trou, et la seconde est tracee en pointilles.

Au-dela de l heure en cours, « reel » et « bridage » valent NULL et non
zero : un zero dessinerait une chute de production qui n a pas eu lieu.

Defaut trouve en chemin : l ecart moyen comptait l heure EN COURS, mesuree
sur ses seules minutes ecoulees. A 14h05 son reel vaut cinq minutes de
production contre une prevision d heure pleine, soit -92 % qui n existent
pas. La colonne `clos` l exclut. Et la serie mesuree s arrete desormais a
la minute courante et non au debut de l heure, si bien que l heure en cours
est reellement tracee au lieu de valoir zero.

Le partage aujourd hui / demain se fait en SQL, dans le fuseau deja utilise
par le reste de la requete : le conteneur repond en UTC a `date`, il ne
doit pas servir d arbitre.

Fenetre de la carte portee a hier / aujourd hui / demain (graph_span 3d,
span end day offset +1d) : sans cela la prevision, qui est justement ce
qu on veut voir, tombe hors du cadre. Verifie dans le paquet installe :
`span.offset` exige bien un signe, et l offset decale les deux bornes.

Verification en production : 47 points de prevision publies, du 17/09 01 h
au 18/09 23 h, 33,3 kWh encore attendus aujourd hui et 34,5 annonces pour
demain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
2026-09-17 01:54:10 +02:00

451 lines
18 KiB
YAML
Raw 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.
views:
# ===================================================================
- title: Bilan
path: bilan
icon: mdi:scale-balance
cards:
- type: vertical-stack
cards:
- type: custom:mushroom-title-card
title: Bilan énergétique
subtitle: Mode ombre — observe et publie, ne pilote rien
- type: conditional
conditions:
- entity: input_text.evcs_balance_status
state: "indisponible"
card:
type: markdown
content: |
## ⚠️ Bilan indisponible
{{ states('input_text.evcs_balance_reason') }}
Les contrôleurs fonctionnent sur leurs règles historiques.
- type: conditional
conditions:
- entity: input_text.evcs_balance_status
state: "degrade"
card:
type: markdown
content: |
## 🟠 Bilan dégradé
Les chiffres sont publiés, mais ils reposent sur une base
incomplète : soit aucun croisement production/consommation
n'apparaît sur l'horizon, soit la forme de production des heures
de déficit vient d'un repli sur l'enveloppe mesurée.
{{ states('input_text.evcs_balance_reason') }}
# --- LA comparaison qui décidera du câblage ---------------------
- type: markdown
title: Seuil calculé contre seuil fixe
content: |
{% set req = states('input_number.evcs_balance_required_soc') | float(0) %}
{% set fixe = states('input_number.victron_grid_soc_off') | float(0) %}
{% set soc = states('sensor.victron_battery_soc') | float(0) %}
{% set ecart = req - fixe %}
| | |
|---|---:|
| SOC batterie **maintenant** | **{{ soc | round(0) }} %** |
| Seuil **calculé** par le bilan | **{{ req | round(1) }} %** |
| Seuil **fixe** encore en vigueur | {{ fixe | round(0) }} % |
| Écart | {{ '+' if ecart >= 0 else '' }}{{ ecart | round(1) }} points |
{% if req > fixe %}
Le seuil fixe est aujourd'hui **trop bas** : il laisserait la batterie
descendre sous ce qu'il faut pour tenir jusqu'au croisement.
{% elif req < fixe %}
Le seuil fixe est aujourd'hui **trop haut** : il fait importer du
réseau au-delà du nécessaire.
{% else %}
Les deux coïncident.
{% endif %}
*C'est l'accumulation de cet écart, jour après jour, qui décidera si
le bilan prend la main.*
# --- Décision immédiate ----------------------------------------
- type: entities
title: Décision immédiate
state_color: true
entities:
- entity: input_boolean.evcs_balance_grid_needed
name: Réseau nécessaire
- entity: input_text.evcs_balance_crossing_hour
name: Croisement production / consommation
- entity: input_number.evcs_balance_energy_to_cover_kwh
name: Énergie à couvrir d'ici là
- entity: input_number.evcs_balance_grid_energy_needed_kwh
name: Énergie réseau strictement nécessaire
- type: divider
- entity: input_number.evcs_balance_required_soc
name: SOC requis (décision)
- entity: input_number.evcs_balance_required_soc_prudent
name: SOC requis (planification)
# --- La nuit qui vient ------------------------------------------
- type: entities
title: Prochaine traversée déficitaire
entities:
- entity: input_text.evcs_balance_next_deficit_window
name: Fenêtre
- entity: input_number.evcs_balance_next_deficit_kwh
name: Énergie à fournir
- entity: input_number.evcs_balance_next_deficit_required_soc
name: SOC à atteindre avant
- entity: input_number.evcs_balance_storage_shortfall_kwh
name: Capacité de stockage manquante
- type: conditional
conditions:
- entity: input_number.evcs_balance_storage_shortfall_kwh
state_not: "0.0"
card:
type: markdown
content: |
### 🔋 Stockage insuffisant pour cette traversée
Il manque **{{ states('input_number.evcs_balance_storage_shortfall_kwh') }} kWh**
de capacité utile : même chargée à 100 %, la batterie actuelle
({{ states('input_number.evcs_balance_usable_capacity_kwh') }} kWh)
ne passe pas la fenêtre sans réseau.
*Un module US2000C ≈ 2,4 kWh nominal.*
# --- Ce qu'on jette ---------------------------------------------
- type: markdown
title: Production
content: |
{% set bride = is_state('input_boolean.evcs_balance_pv_censored', 'on') %}
{% if bride %}
### 🔻 Production actuellement **bridée**
Motif : *{{ states('input_text.evcs_balance_censor_reason') }}*
Sans injection réseau, l'onduleur ne produit que ce que le système
peut absorber. Le soleil est là, il manque un preneur.
{% else %}
### ☀️ Production **libre**
Rien ne bride l'onduleur en ce moment : la mesure de production
reflète la ressource réelle.
{% endif %}
Surplus attendu pour le véhicule aujourd'hui :
**{{ states('input_number.evcs_balance_vehicle_surplus_kwh') }} kWh**
- type: history-graph
title: SOC réel contre seuil calculé
hours_to_show: 48
entities:
- entity: sensor.victron_battery_soc
name: SOC batterie
- entity: input_number.evcs_balance_required_soc
name: Seuil calculé
- entity: input_number.victron_grid_soc_off
name: Seuil fixe
- type: history-graph
title: Bridage de la production
hours_to_show: 48
entities:
- entity: input_boolean.evcs_balance_pv_censored
name: Production bridée
- entity: switch.edf_general_ok_switch
name: Réseau EDF
- type: entities
title: Charge de la batterie — qui limite ?
entities:
- entity: sensor.victron_battery_soc
name: SOC
- entity: sensor.victron_battery_current
name: Courant pris par le parc
- entity: sensor.victron_max_charge_current_setting
name: ↳ plafond que NOUS imposons (DVCC)
- entity: sensor.airtria_8_us2000c_maximum_allowed_charge_current
name: ↳ plafond que le BMS autorise
- type: divider
- entity: sensor.victron_battery_voltage
name: Tension du parc
- entity: sensor.victron_max_charge_voltage_setting
name: ↳ consigne que NOUS imposons
- entity: sensor.airtria_8_us2000c_maximum_allowed_charging_voltage
name: ↳ tension que le BMS autorise
- entity: input_text.victron_charge_mode
name: Profil Auto Charge Manager
- type: divider
- entity: sensor.batterie_ecart_entre_cellules
name: Écart entre cellules
- entity: sensor.airtria_8_us2000c_tension_de_cellule_elevee
name: Alarme tension de cellule
- entity: sensor.airtria_8_us2000c_nombre_de_modules_bloquant_la_charge
name: Modules bloquant la charge
- type: divider
- entity: sensor.solaire_chargeurs_qui_se_retiennent
name: Chargeurs solaires qui se retiennent
- entity: sensor.victron_pv_power_system
name: Production
- type: markdown
content: >
**Comment lire cette carte.** Le courant pris contre le plafond DVCC
dit *qui* limite la charge. Collé au plafond : c'est nous. Loin en
dessous alors que la tension approche la consigne : c'est la fin de
charge normale. Loin en dessous avec une tension basse : il n'y a
simplement pas assez de soleil.
L'écart entre cellules est le garde-fou quand on relève un plafond —
quelques millivolts, le parc est sain ; s'il s'ouvre, on redescend.
À titre de repère, l'ancien module défaillant atteignait 168 mV.
Les chargeurs qui se retiennent sont le seul signal **direct** de
ressource refusée : sans injection, la production mesurée est
censurée par construction et ne dit rien de ce que le soleil pourrait
donner.
⚠️ *Nombre de modules* et *capacité installée* publiés par le BMS ne
comptent que le groupe maître — 4 modules et 400 Ah pour un parc qui
en a neuf. Ils ne sont pas sur cette carte volontairement.
# ===================================================================
- title: Profils & réglages
path: profils
icon: mdi:chart-bell-curve
cards:
- type: custom:mushroom-title-card
title: Profils mesurés
subtitle: Recalculés chaque nuit à 03:30 sur l'historique réel
- type: markdown
title: Profil horaire de consommation (kWh/h, hors véhicule)
content: |
{% set p50 = states('input_text.evcs_balance_consumption_p50').split(',') %}
{% set p80 = states('input_text.evcs_balance_consumption_p80').split(',') %}
{% set env = states('input_text.evcs_balance_envelope').split(',') %}
{% set cen = states('input_text.evcs_balance_censorship_rate').split(',') %}
{% set ok = p50 | length == 24 and p80 | length == 24 and env | length == 24 and cen | length == 24 %}
{% if not ok %}Profils indisponibles — le recalcul quotidien n'a pas encore publié.
{% else %}
| h | conso typique | conso prudente | production max | censure |
|---:|---:|---:|---:|---:|
{% for i in range(24) %}| {{ '%02d' | format(i) }} | {{ p50[i] }} | {{ p80[i] }} | {{ env[i] }} | {{ (cen[i] | float(1) * 100) | round(0) }} % |
{% endfor %}{% endif %}
*La colonne « censure » indique la part des jours où la batterie était
pleine à cette heure : au-delà de 25 %, la production maximale
observée est minorée et le bilan le signale.*
- type: markdown
content: |
### ⚠️ Taille du parc — en équivalents US2000C
Un **US5000 compte pour DEUX**, il fait 100 Ah contre 50.
| parc réel | valeur à saisir |
|---|---:|
| 4 × US2000C | **4** |
| 4 × US2000C + 4 × US5000 | **12** |
| 8 × US2000C | **8** |
Cette valeur alimente trois calculs à la fois : la capacité utile du
contrôleur, la conversion énergie/SOC du bilan, et l'échelle des
limites de courant batterie. À corriger **après** que les nouveaux
modules aient démarré et que les groupes se soient accouplés.
- type: markdown
title: Capacité utile — calculée, pas saisie
content: |
{% set n = states('input_number.evcs_balance_modules_en_service') | float(7) %}
{% set haut = 0.169 * n / 7 %}
{% set bas = 0.097 * n / 7 %}
{% set dc = 67 * haut + 11 * bas %}
**{{ (dc * 0.955) | round(1) }} kWh** réellement livrés à la maison,
de 100 % au plancher de 22 %.
Déduit de la courbe mesurée le 03/09 sur 576 paliers
({{ (haut * 1000) | round(0) }} Wh par point de SOC au-dessus de 33 %,
{{ (bas * 1000) | round(0) }} Wh en dessous) et du parc déclaré
ci-dessus. **Rien à saisir ici.**
*L'ancien réglage « capacité batterie utile » (15 kWh) ne pilote plus
rien depuis le 03/09 : il ne sert que de garde de complétude au
bilan. Il a été retiré de cette carte pour ne plus induire en erreur.*
- type: entities
title: Réglages
entities:
- entity: input_number.evcs_balance_modules_en_service
name: Parc batterie (équivalents US2000C)
- entity: input_boolean.evcs_balance_enabled
name: Bilan actif (kill-switch)
- type: divider
- entity: input_text.evcs_balance_status
name: Statut
- entity: input_text.evcs_balance_last_run
name: Dernière exécution
- entity: input_text.evcs_balance_reason
name: Explication
- type: markdown
content: |
### À propos
Ce bilan remplace des seuils fixes par des grandeurs mesurées. Il
**n'écrit que dans ses propres entités** `evcs_balance_*` : aucun
interrupteur, aucune consigne de borne, aucune stratégie. Cette
garantie est vérifiée par la suite de tests du dépôt, qui échoue si
une écriture sort de ce périmètre.
Détail de la démarche : `docs/CHANTIER_BILAN_ENERGETIQUE.md`.
# =====================================================
# PREVISION CONTRE REEL (15/09/2026)
# =====================================================
# Les trois series viennent de `sensor.evcs_prevision_contre_reel`, publie
# toutes les dix minutes par le service d'historisation depuis PostgreSQL —
# Home Assistant ne garde aucun historique des previsions, ses input_text
# etant ecrases a chaque publication.
- title: Prévision vs réel
path: prevision
icon: mdi:chart-timeline-variant
cards:
- type: markdown
content: >
## Comment lire ces courbes
**La courbe pleine orange est de l'histoire, la pointillée est la
prévision.** Pour un jour écoulé on affiche ce qui avait été annoncé
*le matin même* — c'est la seule référence qui mesure vraiment la
qualité de la prévision, une version révisée à 18 h connaissant déjà
la journée. Pour les heures à venir on affiche la **dernière**
publication : aujourd'hui jusqu'à ce soir, et demain en entier.
⚠️ **Une heure bridée n'est pas comparable.** Quand la batterie est
pleine et qu'aucun preneur ne se présente, la production se cale sur
la consommation : elle passe sous la prévision sans que la prévision
soit fausse. La bande grise du bas dit quand c'est le cas.
Le chiffre en tête — l'écart moyen sur sept jours — ne compte **que
les heures non bridées et déjà terminées**. C'est le seul qui juge
vraiment la prévision.
Deux exemples mesurés : le 13/09 à 15 h, prévu 5,32 kWh et réalisé
1,82 avec 100 % de bridage — l'écart n'est pas météorologique. Le
15/09 à 13 h, prévu 3,92 et réalisé 7,12 sans aucun bridage — là, la
prévision **sous-estime** de 82 %.
- type: entities
entities:
- type: attribute
entity: sensor.evcs_prevision_contre_reel
attribute: reste_aujourdhui_kwh
name: Encore attendu aujourd'hui
suffix: " kWh"
icon: mdi:weather-sunny
- type: attribute
entity: sensor.evcs_prevision_contre_reel
attribute: annonce_demain_kwh
name: Annoncé pour demain
suffix: " kWh"
icon: mdi:weather-sunset-up
- type: divider
- entity: sensor.evcs_prevision_contre_reel
name: Écart moyen (heures comparables, 7 j)
- type: attribute
entity: sensor.evcs_prevision_contre_reel
attribute: heures_comparables
name: Heures comparables
- type: attribute
entity: sensor.evcs_prevision_contre_reel
attribute: heures_total
name: Heures terminées dans la fenêtre
- type: custom:apexcharts-card
header:
show: true
title: Production réalisée, et ce qui est annoncé jusqu'à demain soir
show_states: false
# Hier, aujourd'hui, demain. La fenêtre s'arrête a la fin de DEMAIN et
# non a l'heure en cours : sans cela la prevision, qui est justement ce
# qu'on veut voir, tombe hors du cadre.
graph_span: 3d
span:
end: day
offset: '+1d'
apex_config:
chart:
height: 340
stroke:
width: [1, 2, 2]
dashArray: [0, 0, 5]
legend:
position: top
yaxis:
- title: {text: kWh par heure}
decimals: 1
min: 0
series:
- entity: sensor.evcs_prevision_contre_reel
name: Réalisée
type: area
opacity: 0.25
color: '#2d8cf0'
data_generator: |
return (entity.attributes.reelle || [])
.filter(p => p[1] !== null && p[1] !== undefined);
- entity: sensor.evcs_prevision_contre_reel
name: Prévue le matin même
type: line
color: '#f0a202'
data_generator: |
return (entity.attributes.predite || [])
.filter(p => p[1] !== null && p[1] !== undefined);
- entity: sensor.evcs_prevision_contre_reel
name: Prévision en cours
type: line
color: '#f05a02'
data_generator: |
return (entity.attributes.attendue || [])
.filter(p => p[1] !== null && p[1] !== undefined);
- type: custom:apexcharts-card
header:
show: true
title: Part de l'heure où la production était bridée
show_states: false
graph_span: 3d
span:
end: day
offset: '+1d'
apex_config:
chart:
height: 150
yaxis:
- min: 0
max: 100
title: {text: '% de l heure'}
legend:
show: false
series:
- entity: sensor.evcs_prevision_contre_reel
name: Bridage
type: area
opacity: 0.4
color: '#9e9e9e'
data_generator: |
return (entity.attributes.bridage || [])
.filter(p => p[1] !== null && p[1] !== undefined);