Gestion Chauffage - blueprint

Bonjour,

Je pose ça là pour une gestion du chauffage avec des radiateur électrique.

contexte :

  • maison principalement chauffé au bois avec des radiateur électrique (1000w)
  • relais zigbee de type interrupteur pour chaque radiateur :
    Ça chauffe à 1000w ou ça chauffe pas :smiley:
  • capteur de température dans chaque pièce
  • gestion du chauffage par calendrier

L’IA a fait pas mal le taf de codage

et Bonne année !

:magnifying_glass_tilted_left: Description

Ce blueprint avancé pour Home Assistant permet de gérer le chauffage multi-pièces de manière intelligente et économe. Il combine :

  • Calendrier : applique automatiquement un préréglage (eco, confort, etc.) en fonction des événements.
  • Géorepérage : bascule en mode absence si toutes (ou certaines) personnes sont éloignées du domicile.
  • Forçage global : possibilité de forcer un préréglage pour toute la maison pendant une durée définie.
  • Forçage par pièce : chaque pièce peut être forcée individuellement.
  • Limiteur de créneaux (max_slots) : limite le nombre de pièces qui chauffent simultanément pour éviter les surcharges.
  • Priorité : définit quelles pièces sont servies en premier si la capacité est limitée.
  • Fallback : applique un préréglage de repli si une pièce demande à chauffer mais n’a pas de créneau disponible.
  • Logbook Debug : option pour suivre les décisions dans le journal.

:white_check_mark: Idéal pour optimiser la consommation énergétique tout en gardant le confort.


:hammer_and_wrench: Prérequis

  • Home Assistant ≥ 2023.x
  • Entités nécessaires :
    • input_boolean pour activer/désactiver l’automatisation
    • calendar pour les événements (message → préréglage)
    • person pour le géorepérage
    • climate pour les radiateurs/pièces
    • timer pour la durée du forçage global
    • input_boolean par pièce pour le forçage individuel

:open_book: Guide d’installation et d’utilisation

:one: Importer le Blueprint

  • Télécharge le fichier YAML (ou copie-colle le code dans l’éditeur de blueprint).
  • Sauvegarde.

:two: Configurer les Inputs

Lors de la création de l’automatisation à partir du blueprint :

  • Mode chauffage : input_boolean maître pour activer/désactiver.
  • Calendrier : entité calendar qui contient les événements (ex. “confort”, “eco”).
  • Personnes : liste des entités person pour le géorepérage.
  • Seuil de distance : en km (ex. 150 km).
  • Règle d’absence : ON = toutes les personnes doivent être loin, OFF = au moins une suffit.
  • Forçage global : input_boolean + timer + durée (en minutes).
  • Préréglage global forcé : eco, confort, etc.
  • Pièces (climate) : liste des entités climate à piloter.
  • Ordre de priorité : facultatif, sinon ordre des pièces.
  • Forçage par pièce : liste d’input_boolean (même ordre que les pièces).
  • Préréglages “chauffage” : ex. home, comfort.
  • Fallback : préréglage appliqué si pas de créneau (ex. eco).
  • Mapping calendrier → préréglage : personnalise selon tes événements.
  • Préréglage par défaut : si message inconnu.
  • Max slots : nombre max de pièces chauffant simultanément (hors forcées).
  • Debug logbook : active pour voir les décisions dans le journal.

:three: Logique de fonctionnement

  • Si forçage global ON → applique le préréglage global forcé.
  • Sinon, si toutes/une personne est loin → applique “away”.
  • Sinon → applique le préréglage du calendrier.
  • Les pièces forcées sont toujours servies.
  • Les autres pièces sont limitées par max_slots et priorisées.
  • Si une pièce demande à chauffer mais n’a pas de slot → applique le fallback.

:white_check_mark: Exemple d’usage

  • Événement calendrier “confort” → toute la maison passe en confort.
  • Si tout le monde est à plus de 150 km → passe en “away”.
  • Forçage global ON pendant 4h → tout en “eco”.
  • Radiateur du salon forcé → chauffe même si slots pleins.

:desktop_computer: Informations système (compatibilité vérifiée)

version: core-2025.12.5
installation_type: Home Assistant OS
docker: true | arch: amd64 | OS: Linux 6.12.51-haos
python_version: 3.13.9
timezone: Europe/Paris
Supervisor: supervisor-2025.12.3 | Host OS: Home Assistant OS 16.3
Add-ons: File editor, Studio Code Server, ESPHome, Zigbee2MQTT, Mosquitto, AdGuard, Music Assistant...
Cloud: Nabu Casa actif (remote OK)

:white_check_mark: Testé sur Home Assistant OS, architecture x86_64, avec HACS et intégrations courantes.


blueprint:
  name: "Chauffage — Calendrier + Géorepérage + Forçage Global/Par Pièce + Limiteur de créneaux + Priorité (Anonymisé)"
  description: >
    Applique un préréglage global déterminé par un calendrier et des règles de géorepérage. Gère un
    forçage global (avec minuterie) et un forçage par pièce. Limite le nombre de pièces non forcées
    qui chauffent simultanément (max_slots). Une pièce déjà en chauffe et en dessous de sa consigne
    termine son cycle avant d’être rétrogradée (sauf si forcée). Un ordre de priorité contrôle quelles
    pièces obtiennent des créneaux de chauffe en premier.
  domain: automation

  input:
    mode_switch:
      name: "Mode chauffage (activer/désactiver l’automatisation)"
      description: "Interrupteur maître. Quand OFF, le blueprint n’exécute aucune action."
      selector:
        entity:
          domain: input_boolean

    calendar_entity:
      name: "Calendrier pour le préréglage (message → préréglage)"
      description: "Événement du calendrier dont le champ 'message' est mappé vers un préréglage via 'preset_mapping'."
      selector:
        entity:
          domain: calendar

    persons:
      name: "Personnes suivies pour le géorepérage"
      description: "Liste des entités 'person' utilisées pour calculer la distance à la maison (zone.home)."
      selector:
        entity:
          multiple: true
          domain: person

    distance_threshold:
      name: "Seuil de distance du géorepérage (km)"
      description: "Distance (en km) au-delà de laquelle une personne est considérée 'loin' de la maison."
      default: 150
      selector:
        number:
          min: 10
          max: 1000
          unit_of_measurement: km
          mode: slider

    enforce_all_far:
      name: "Règle d’absence"
      description: "ON : TOUTES les personnes doivent être au-delà du seuil. OFF : AU MOINS UNE suffit."
      default: true
      selector:
        boolean: {}

    global_force_boolean:
      name: "Forçage global (input_boolean)"
      description: "Interrupteur pour forcer un préréglage global pendant une durée définie."
      selector:
        entity:
          domain: input_boolean

    global_force_timer:
      name: "Minuterie du forçage global"
      description: "Minuterie associée au forçage global. Démarre/annule automatiquement."
      selector:
        entity:
          domain: timer

    global_force_minutes:
      name: "Durée du forçage global (minutes)"
      description: "Durée appliquée lorsque le forçage global passe à ON."
      default: 240
      selector:
        number:
          min: 5
          max: 1440
          unit_of_measurement: min
          mode: slider

    global_force_preset:
      name: "Préréglage pendant le forçage global"
      description: "Préréglage appliqué quand le forçage global est actif."
      default: eco
      selector:
        select:
          options:
            - eco
            - home
            - comfort
            - away
            - sleep

    climates:
      name: "Pièces (entités climate)"
      description: "Liste des entités climate à piloter (une par pièce ou par radiateur)."
      selector:
        entity:
          multiple: true
          domain: climate

    priority_order:
      name: "Ordre de priorité (mêmes entités que 'Pièces', de la plus prioritaire à la moins prioritaire)"
      description: "Facultatif. Si vide, l’ordre suit la liste des 'Pièces'. Sert à attribuer les créneaux de chauffe en premier."
      selector:
        entity:
          multiple: true
          domain: climate
      default: []

    per_radiator_force_booleans:
      name: "Forçages par pièce (input_booleans ; même ordre que 'Pièces')"
      description: "Liste d’input_booleans 1:1 avec 'Pièces'. Quand ON pour une pièce, elle est forcée en chauffe."
      selector:
        entity:
          multiple: true
          domain: input_boolean
      default: []

    heating_presets:
      name: "Préréglages considérés comme 'chauffage'"
      description: "Toute pièce demandant l’un de ces préréglages est considérée en demande de chauffe."
      default:
        - home
        - comfort
      selector:
        select:
          multiple: true
          options:
            - home
            - comfort
            - presence

    fallback_non_heating:
      name: "Préréglage de repli si une pièce demande à chauffer sans créneau disponible"
      description: "Préréglage à appliquer aux pièces en demande mais sans slot disponible (non forcées)."
      default: eco
      selector:
        select:
          options:
            - eco
            - away
            - sleep

    preset_mapping:
      name: "Mapping message de calendrier → préréglage (objet)"
      description: "Dictionnaire qui convertit le champ 'message' de l’événement calendrier en préréglage cible."
      default:
        eco: eco
        confort: comfort
        comfort: comfort
        home: home
        presence: home
        away: away
        absent: away
        nuit: sleep
        sleep: sleep
      selector:
        object: {}

    default_preset:
      name: "Préréglage par défaut si le message du calendrier est inconnu"
      description: "Utilisé quand 'message' ne correspond à aucune clé du mapping."
      default: eco
      selector:
        select:
          options:
            - eco
            - away
            - home
            - comfort
            - sleep

    max_slots:
      name: "Nombre max. de pièces non forcées qui chauffent simultanément"
      description: "Capacité en parallèle pour les pièces non forcées (les pièces forcées ne comptent pas dans ce quota)."
      default: 2
      selector:
        number:
          min: 1
          max: 10
          mode: slider

    debug_logbook:
      name: "Activer les logs de débogage (Logbook)"
      description: "Si ON, écrit un résumé des listes calculées dans le Logbook."
      default: true
      selector:
        boolean: {}

mode: restart

# ---- Déclencheurs (ici, !input est nécessaire) ----
trigger:
  - platform: calendar
    entity_id: !input calendar_entity
    event: start
    id: cal_start

  - platform: calendar
    entity_id: !input calendar_entity
    event: end
    id: cal_end

  - platform: state
    entity_id: !input mode_switch
    to: "on"
    id: mode_on

  - platform: state
    entity_id: !input persons
    id: presence_change

  - platform: homeassistant
    event: start
    id: hass_start

  - platform: state
    entity_id: !input global_force_boolean
    to: "on"
    id: force_on

  - platform: state
    entity_id: !input global_force_boolean
    to: "off"
    id: force_off

  - platform: event
    event_type: timer.finished
    event_data:
      entity_id: !input global_force_timer
    id: timer_finished

  - platform: state
    entity_id: !input per_radiator_force_booleans
    id: per_room_force_change

  # Réallocation des créneaux au début/fin d’un cycle
  - platform: state
    entity_id: !input climates
    attribute: hvac_action
    id: hvac_action_change

# ---- Condition (ici aussi, !input est requis) ----
condition:
  - condition: state
    entity_id: !input mode_switch
    state: "on"

# ---- Liaisons d'inputs vers variables "id" (pour éviter !input dans les templates) ----
variables:
  calendar_entity_id: !input calendar_entity
  mode_switch_id: !input mode_switch
  persons_ids: !input persons
  distance_threshold: !input distance_threshold
  enforce_all_far: !input enforce_all_far
  global_force_boolean_id: !input global_force_boolean
  global_force_timer_id: !input global_force_timer
  global_force_minutes_val: !input global_force_minutes
  global_force_preset_val: !input global_force_preset
  climates_ids: !input climates
  priority_order_in: !input priority_order
  force_booleans_ids: !input per_radiator_force_booleans
  heating_presets: !input heating_presets
  fallback_non_heating: !input fallback_non_heating
  preset_mapping: !input preset_mapping
  default_preset: !input default_preset
  max_slots: !input max_slots
  debug_logbook: !input debug_logbook

  # Calendrier → préréglage (utilise la variable calendar_entity_id, pas !input)
  cal_msg: >-
    {{ state_attr(calendar_entity_id, 'message') | default('', true) | lower | trim }}
  cal_preset: >-
    {% set p = preset_mapping.get(cal_msg) %}
    {{ p if p is not none else default_preset }}

action:
  # ===== État global / géorepérage =====
  - variables:
      distances: >-
        {% set res = [] %}
        {% for p in persons_ids %}
          {% set res = res + [distance(p, 'zone.home') | float(0)] %}
        {% endfor %}
        {{ res }}
      both_far: >-
        {% set cnt_far = 0 %}
        {% for d in distances %}
          {% if d > distance_threshold %}
            {% set cnt_far = cnt_far + 1 %}
          {% endif %}
        {% endfor %}
        {% if enforce_all_far %}
          {{ cnt_far == (distances | count) }}
        {% else %}
          {{ cnt_far > 0 }}
        {% endif %}
      global_force_on: "{{ is_state(global_force_boolean_id, 'on') }}"
      target_preset_global: >-
        {% if global_force_on %}
          {{ global_force_preset_val }}
        {% elif both_far %}
          {{ 'away' }}
        {% else %}
          {{ cal_preset }}
        {% endif %}

  # ===== Gestion du timer de forçage global (durée en minutes) =====
  - choose:
      - conditions:
          - condition: trigger
            id: force_on
        sequence:
          - service: timer.start
            target:
              entity_id: "{{ global_force_timer_id }}"
            data:
              duration: >-
                {% set m = (global_force_minutes_val | int) %}
                {{ '%02d:%02d:00' | format((m // 60) | int, (m % 60) | int) }}
      - conditions:
          - condition: or
            conditions:
              - condition: trigger
                id: force_off
              - condition: trigger
                id: timer_finished
        sequence:
          - service: input_boolean.turn_off
            target:
              entity_id: "{{ global_force_boolean_id }}"
          - service: timer.cancel
            target:
              entity_id: "{{ global_force_timer_id }}"

  # ===== Construction des listes : forcées / à terminer / en demande =====
  - variables:
      forced_list: >-
        {% set res = [] %}
        {% for cid in climates_ids %}
          {% set idx = loop.index0 %}
          {% set b = force_booleans_ids[idx] if (force_booleans_ids | count > idx) else none %}
          {% if b is not none and is_state(b, 'on') %}
            {% set res = res + [cid] %}
          {% endif %}
        {% endfor %}
        {{ res }}

      must_finish_list: >-
        {% set res = [] %}
        {% for cid in climates_ids %}
          {% set hv = state_attr(cid, 'hvac_action') | default('unknown') %}
          {% set cur = state_attr(cid, 'current_temperature') | float(0) %}
          {% set tgt = state_attr(cid, 'temperature') | float(0) %}
          {% set heating = (hv == 'heating') or (hv == 'unknown' and cur < tgt) %}
          {% if heating and (cur < tgt) %}
            {% set res = res + [cid] %}
          {% endif %}
        {% endfor %}
        {{ res }}

      wants_list: >-
        {% set res = [] %}
        {% for cid in climates_ids %}
          {% set req = 'home' if (cid in forced_list) else target_preset_global %}
          {% if req in heating_presets %}
            {% set res = res + [cid] %}
          {% endif %}
        {% endfor %}
        {{ res }}

      selected_init: >-
        {% set s = forced_list %}
        {% for cid in must_finish_list %}
          {% if cid not in s %}
            {% set s = s + [cid] %}
          {% endif %}
        {% endfor %}
        {{ s }}

      capacity: >-
        {% set count_non_forced = 0 %}
        {% for cid in selected_init %}
          {% if cid not in forced_list %}
            {% set count_non_forced = count_non_forced + 1 %}
          {% endif %}
        {% endfor %}
        {% set cap = max_slots - count_non_forced %}
        {{ 0 if cap < 0 else cap }}

      priority: >-
        {% set prio = priority_order_in %}
        {% for cid in climates_ids %}
          {% if cid not in prio %}
            {% set prio = prio + [cid] %}
          {% endif %}
        {% endfor %}
        {{ prio }}

      selected_final: >-
        {% set selected = selected_init %}
        {% set cap = capacity | int %}
        {% for cid in priority %}
          {% if cap > 0 and (cid not in forced_list) and (cid in wants_list) and (cid not in selected) %}
            {% set selected = selected + [cid] %}
            {% set cap = cap - 1 %}
          {% endif %}
        {% endfor %}
        {{ selected }}

  # ===== Application des préréglages finaux par pièce =====
  - repeat:
      # NOTE : si votre version de HA n’accepte pas le template ici, remettez : for_each: !input climates
      for_each: "{{ climates_ids }}"
      sequence:
        - variables:
            cid: "{{ repeat.item }}"
            heat: "{{ cid in selected_final }}"
            req: "{{ 'home' if (cid in forced_list) else target_preset_global }}"
            final_preset: >-
              {% if heat %}
                {{ req }}
              {% else %}
                {% if req in heating_presets %}
                  {{ fallback_non_heating }}
                {% else %}
                  {{ req }}
                {% endif %}
              {% endif %}
        - service: climate.set_preset_mode
          target:
            entity_id: "{{ cid }}"
          data:
            preset_mode: "{{ final_preset }}"

  # ===== Journal de débogage optionnel =====
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ debug_logbook }}"
        sequence:
          - service: logbook.log
            data:
              name: "Chauffage (Blueprint)"
              message: >-
                cal={{ cal_preset }} | target_global={{ target_preset_global }}
                | distances={{ distances | map('round', 1) | list }} km
                | forced={{ forced_list }}
                | must_finish={{ must_finish_list }}
                | wants={{ wants_list }}
                | selected_final={{ selected_final }}
              entity_id: "{{ climates_ids | first }}"