diff --git a/packages/evcs_expiration_force_grid.yaml b/packages/evcs_expiration_force_grid.yaml new file mode 100755 index 0000000..8dbe37b --- /dev/null +++ b/packages/evcs_expiration_force_grid.yaml @@ -0,0 +1,146 @@ +# Expiration du reseau force — le contournement manuel a desormais une fin +# +# LE DEFAUT, constate le 17/09/2026. `force_grid_now` pose a la main a 12:58 +# pendant les heures creuses de midi (12:24-14:24) a ferme le contacteur et +# lance une charge batterie a 7,7 kW. Rien dans le systeme ne l'arrete : +# +# - WINDOWS_SOURCEV2 : `else if (forceGridRequested) sourceMode='grid_forced'` +# est evalue AVANT tout test de profil. La fenetre HC n'entre pas dans le +# calcul. +# - Hysteresis : `evcsExplicitGridRequest` inclut la strategie, et l'override +# ne tombe que sur un motif « pas de demande » — or `vehicle_not_connected` +# n'en fait pas partie. Le depart du vehicule ne libere donc rien. +# - Les deux coupures immediates existantes sont bloquees par cet override. +# +# Consequence : a 14:24 la fenetre economique se ferme et l'import continue AU +# PLEIN TARIF, indefiniment, jusqu'a ce qu'un humain soit devant l'ecran. +# +# LA REGLE, et pourquoi elle est formulee ainsi. Un contournement manuel n'est +# pas illegitime — le proprietaire ferme deliberement le contacteur pour +# charger en heures creuses, ou pour proteger le parc pendant une pointe. Ce +# qui manque n'est pas une interdiction, c'est une ECHEANCE. Deux sorties, et +# aucune ne repose sur un seuil invente : +# +# 1. FENETRE ECONOMIQUE. Si le reseau force a ete engage PENDANT une fenetre +# d'heures creuses, il expire a la sortie de cette fenetre. On n'achete +# pas au plein tarif ce qu'on avait decide d'acheter au tarif creux. +# ⚠️ La condition porte sur l'instant d'ENGAGEMENT, pas seulement sur +# l'heure courante : engage HORS heures creuses — pour proteger le parc un +# soir de cuisson, cf. la regle « fermer EDF pour proteger le materiel » — +# il ne doit PAS etre annule a la minute suivante. C'est la difference +# entre une echeance et une interdiction. +# 2. PLAFOND DE DUREE. Au-dela de `evcs_force_grid_max_minutes`, quelle que +# soit la raison. Filet pour le cas engage hors fenetre et oublie. +# +# Les bornes des fenetres sont LUES dans les helpers qui font foi +# (`victron_evcs_hc_*`), jamais recopiees : elles ont deja change une fois +# (01:30 -> 01:24 le 09/07/2026) et une copie divergerait en silence. +# +# ⚠️ AUCUN `initial:` DANS CE FICHIER. Declarer `initial:` DESACTIVE la +# restauration d'etat : le helper reprend cette valeur a CHAQUE demarrage de +# Home Assistant. Le 06/09/2026, cinq reglages manuels du projet se +# reinitialisaient ainsi en silence, dont deux toggles de cablage. + +input_boolean: + evcs_expiration_force_grid_enabled: + name: "EVCS — expiration du réseau forcé" + icon: mdi:timer-sand + +input_number: + evcs_force_grid_max_minutes: + name: "EVCS — durée maximale du réseau forcé" + min: 15 + max: 720 + step: 15 + unit_of_measurement: min + mode: box + icon: mdi:timer-alert-outline + +input_text: + # Trace lisible du dernier declenchement. Un helper `input_text` accepte 100 + # caracteres par defaut et REJETTE EN SILENCE au-dela — le 08/09/2026 une + # panne de 4 h 45 n'a laisse aucune trace pour cette raison. 255 ici. + evcs_force_grid_last_expiry: + name: "EVCS — dernière expiration du réseau forcé" + max: 255 + icon: mdi:history + +automation: + - id: evcs_expiration_reseau_force + alias: "EVCS — le réseau forcé expire tout seul" + description: > + Repasse la stratégie en auto_solar_plus quand le réseau forcé a perdu sa + raison d'être : sortie de la fenêtre d'heures creuses dans laquelle il a + été engagé, ou dépassement de la durée maximale. + mode: single + trigger: + - platform: time_pattern + minutes: "/1" + condition: + - condition: state + entity_id: input_select.victron_evcs_strategy + state: force_grid_now + - condition: state + entity_id: input_boolean.evcs_expiration_force_grid_enabled + state: "on" + - condition: template + value_template: > + {% set s = states.input_select.victron_evcs_strategy %} + {% set ns = states('input_datetime.victron_evcs_hc_night_start').split(':') %} + {% set ne = states('input_datetime.victron_evcs_hc_night_stop').split(':') %} + {% set ms = states('input_datetime.victron_evcs_hc_midday_start').split(':') %} + {% set me = states('input_datetime.victron_evcs_hc_midday_stop').split(':') %} + {% set n1 = ns[0]|int * 60 + ns[1]|int %} + {% set n2 = ne[0]|int * 60 + ne[1]|int %} + {% set m1 = ms[0]|int * 60 + ms[1]|int %} + {% set m2 = me[0]|int * 60 + me[1]|int %} + {% set eng = s.last_changed | as_local %} + {% set em = eng.hour * 60 + eng.minute %} + {% set hm = now().hour * 60 + now().minute %} + {% set d = namespace(eng=false, now=false) %} + {% if n1 <= n2 %} + {% set d.eng = (em >= n1 and em < n2) %} + {% set d.now = (hm >= n1 and hm < n2) %} + {% else %} + {% set d.eng = (em >= n1 or em < n2) %} + {% set d.now = (hm >= n1 or hm < n2) %} + {% endif %} + {% if em >= m1 and em < m2 %}{% set d.eng = true %}{% endif %} + {% if hm >= m1 and hm < m2 %}{% set d.now = true %}{% endif %} + {% set duree = (as_timestamp(now()) - as_timestamp(s.last_changed)) / 60 %} + {% set plafond = states('input_number.evcs_force_grid_max_minutes') | float(180) %} + {{ (d.eng and not d.now) or (duree > plafond) }} + action: + - variables: + motif: > + {% set s = states.input_select.victron_evcs_strategy %} + {% set duree = ((as_timestamp(now()) - as_timestamp(s.last_changed)) / 60) | round(0) %} + {% set plafond = states('input_number.evcs_force_grid_max_minutes') | float(180) %} + {% if duree > plafond %}durée maximale de {{ plafond | round(0) }} min dépassée + {% else %}sortie de la fenêtre d'heures creuses{% endif %} + - service: input_select.select_option + target: + entity_id: input_select.victron_evcs_strategy + data: + option: auto_solar_plus + - service: input_text.set_value + target: + entity_id: input_text.evcs_force_grid_last_expiry + data: + value: > + {{ now().strftime('%d/%m %H:%M') }} — retour auto_solar_plus + ({{ motif }}), SOC {{ states('sensor.victron_battery_soc') | round(0) }} % + - service: logbook.log + data: + name: EVCS + message: > + Réseau forcé expiré ({{ motif }}) — retour à auto_solar_plus. + SOC {{ states('sensor.victron_battery_soc') | round(0) }} %. + entity_id: input_select.victron_evcs_strategy + - service: persistent_notification.create + data: + title: EVCS — réseau forcé expiré + message: > + La stratégie est repassée en **auto_solar_plus** : {{ motif }}. + SOC {{ states('sensor.victron_battery_soc') | round(0) }} %. + notification_id: evcs_force_grid_expire