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); # =================================================================== # Production du jour PAR CHARGEUR (25/09/2026). Source unique : # sensor.evcs_production_jour_kwh, publie par le service d'historisation # (tools/diagnostics-agent.mjs) — le meme chiffre que l'agent vocal et que # Grafana « Le solaire ». Il gere les remises a zero decalees des trois # chargeurs (vers 00:40) et signale un chargeur muet au lieu de sommer un trou. - title: Production path: production icon: mdi:solar-power cards: # Source de la prevision du bilan (28/09/2026) : VRM en essai, comparaison # prevue le 05/10. Revenir a forecast_solar = retour arriere immediat. - type: entities title: Source de la prévision solaire du bilan entities: - entity: input_select.evcs_balance_forecast_source name: Source utilisée - entity: input_text.evcs_balance_vrm_updated name: VRM mise à jour - entity: input_text.evcs_balance_forecast_updated name: Forecast.Solar mise à jour - entity: sensor.evcs_prevision_contre_reel name: Écart réel / prévision (7 j, source active) - type: markdown content: > **VRM** : prévision de Victron, calculée sur l'historique réel de l'installation. **Forecast.Solar** : prévision météo sur les trois plans. Les deux sont enregistrées chaque jour ; comparaison le **05/10** pour décider laquelle garder. - type: markdown title: Production du jour par chargeur content: | {% set p = 'sensor.evcs_production_jour_kwh' %} {% set st = states(p) %} {% set d = state_attr(p, 'detail_kwh') or {} %} {% set statut = state_attr(p, 'statut') %} {% if st in ['unknown', 'unavailable', 'indisponible'] %} ### ⚠️ Production du jour inconnue {{ state_attr(p, 'explication') or "Le service d'historisation n'a pas encore publie la valeur (quelques secondes apres un redemarrage de Home Assistant)." }} {% else %} ## ☀️ {{ st }} kWh aujourd'hui {% if statut == 'partiel' %} ⚠️ **Total partiel** — chargeur muet : {{ (state_attr(p, 'chargeurs_muets') or []) | join(', ') }}. La production réelle est plus haute. {% endif %} | Chargeur | Panneaux | kWh | |---|---|---:| | MPPT 250/70 — sud-est | 10 | {{ d.airtria_mppt1 if d.airtria_mppt1 is not none else 'muet' }} | | RS 450/200 — sud + ouest | 39 | {{ d.mppt_450_200 if d.mppt_450_200 is not none else 'muet' }} | {%- set noms = {1: 'sud, string 1', 2: 'sud, string 2', 3: 'sud, string 3', 4: 'ouest'} %} {%- set pan = {1: 10, 2: 10, 3: 10, 4: 9} %} {%- for i in [1, 2, 3, 4] %} {%- set e = 'sensor.mppt_450_200_hq2449yr9pt_tracker_solar_' ~ i ~ '_yield_today' %} {%- set v = states(e) %} {#- meme regle que le capteur total : une valeur d'hier pas encore remise a zero compte 0 #} {%- set hier = v | float(0) > 0 and states[e] is not none and states[e].last_changed < today_at('00:00') %} | ↳ {{ noms[i] }} | {{ pan[i] }} | {{ 0 if hier else (v if v not in ['unknown', 'unavailable'] else 'muet') }} | {%- endfor %} | 150/35 — suiveur | 3 | {{ d.v1 if d.v1 is not none else 'muet' }} | {% if state_attr(p, 'pas_encore_remis_a_zero') %} *Pas encore remis à zéro (valeur d'hier comptée 0) : {{ state_attr(p, 'pas_encore_remis_a_zero') | join(', ') }}.* {% endif %} {% endif %} *C'est la production **réelle** (rendement déclaré par chaque chargeur), pas la prévision. Sans injection, elle ne dit pas ce que le soleil aurait pu donner : une journée bridée produit moins qu'elle ne pourrait.* - type: custom:apexcharts-card header: show: true title: Cumul de la journée par chargeur show_states: false graph_span: 1d span: start: day apex_config: chart: height: 300 legend: position: top yaxis: - title: {text: kWh} decimals: 1 min: 0 series: - entity: sensor.evcs_production_jour_kwh name: Total type: line color: '#f0a202' stroke_width: 3 - entity: sensor.mppt_450_200_hq2449yr9pt_yield_today name: RS 450/200 sud + ouest type: line color: '#2d8cf0' - entity: sensor.airtria_mppt1_yield_today name: MPPT 250/70 sud-est type: line color: '#19be6b' - entity: sensor.v1_yield_today name: 150/35 suiveur type: line color: '#9e9e9e' - type: custom:apexcharts-card header: show: true title: RS 450/200 — les quatre entrées show_states: false graph_span: 1d span: start: day apex_config: chart: height: 260 legend: position: top yaxis: - title: {text: kWh} decimals: 1 min: 0 # Les trois strings sud (10 panneaux, 35 degres) doivent rester # superposees : une courbe qui decroche signale un defaut de string # (ombre, connecteur, panneau). L'ouest (9 panneaux) produit l'apres-midi. series: - entity: sensor.mppt_450_200_hq2449yr9pt_tracker_solar_1_yield_today name: Sud string 1 color: '#2d8cf0' - entity: sensor.mppt_450_200_hq2449yr9pt_tracker_solar_2_yield_today name: Sud string 2 color: '#19be6b' - entity: sensor.mppt_450_200_hq2449yr9pt_tracker_solar_3_yield_today name: Sud string 3 color: '#9a66e4' - entity: sensor.mppt_450_200_hq2449yr9pt_tracker_solar_4_yield_today name: Ouest color: '#f05a02' - type: markdown content: > Les quatre entrées du RS 450/200 sont un **détail** de son total : ne jamais les additionner au total du jour (double comptage). L'historique sur plusieurs jours est dans **Grafana**, tableau « Le solaire », panneau « Production du jour par chargeur ».