Calculer son "Ressenti Réel" pour automatiser ses fenêtres et volets comme un pro

La plupart des automatisations de gestion thermique en domotique se basent sur la température de l’air sous abri. Pourtant, $25^\circ\text{C}$ à l’ombre avec du mistral ne procurent pas du tout la même sensation que $25^\circ\text{C}$ plein soleil sans vent.

Aujourd’hui, nous allons voir comment créer un index de Ressenti Réel dynamique (basé sur l’Humidex, le vent moyen et l’inclinaison des rayons solaires) pour piloter intelligemment l’ouverture des fenêtres et la fermeture des volets roulants.

🛠️Prérequis et matériel

Pour reproduire ce tutoriel, vous aurez besoin de données météo précises.

La configuration idéale (Station locale)

  • Une station météo Ecowitt (ex: passerelle Ecowitt GW2000A avec ses capteurs).
  • Un capteur d’ultraviolets / radiation solaire (intégré sur la majorité des blocs de capteurs extérieurs Ecowitt).
  • Un anémomètre pour la vitesse du vent.
  • Humidex : L’intégration Thermal Comfort (disponible via HACS) pour générer automatiquement l’index Humidex à partir de n’importe quel couple température/humidité.
 

L’alternative minimale (Sans station météo)
Si vous n’avez pas de station locale, vous pouvez utiliser des intégrations logicielles gratuites pour récupérer ces données :

  • Météo & Vent : L’intégration officielle Météo-France ou OpenWeatherMap.
  • Soleil & Inclinaison : L’intégration native sun de Home Assistant (fournit l’azimut et l’élévation).
  • Humidex : L’intégration Thermal Comfort (disponible via HACS) pour générer automatiquement l’index Humidex à partir de n’importe quel couple température/humidité.

🧠 Étape 1 : Le calcul mathématique du "Ressenti Réel"

Le vent refroidit notre corps, tandis que l’énergie solaire le réchauffe. Mais l’impact du soleil dépend de sa hauteur dans le ciel : un soleil rasant à l’aube chauffe beaucoup moins qu’un soleil au zénith.

📋 Prérequis et capteurs nécessaires

Pour que ce calcul fonctionne, vous devez disposer des entités suivantes dans votre système :

  • sensor.thermal_comfort_humidex : Généré via l’intégration Thermal Comfort (HACS).

  • sensor.exterieur_gw2000a_moyenne_vent : La vitesse du vent. Conseil : Utilisez une entrée de type « Statistiques » (Moyenne glissante sur 10 minutes) dans HA pour lisser les rafales et éviter l’effet yoyo.

  • sensor.gw2000a_solar_radiation : Le rayonnement global en $W/m²$ (fourni par votre station ou une alternative).

  • sensor.sun_solar_elevation : L’angle d’élévation du soleil (fourni nativement par l’intégration sun de Home Assistant).


💻 Code du Template Sensor

Créez un nouveau Template Sensor (via l’interface ou votre fichier configuration.yaml) et collez ce code :

📋
Template.yml
Copy to clipboard
{% set humidex = states('sensor.thermal_comfort_humidex') | float(0) %}
{% set vent = states('sensor.exterieur_gw2000a_moyenne_vent') | float(0) %}
{% set solar = states('sensor.gw2000a_solar_radiation') | float(0) %}
{% set elevation = states('sensor.sun_solar_elevation') | float(0) %}

{# 1. CALCUL DE L'EFFET DU VENT #}
{# Plus l'air est lourd (Humidex élevé), moins le vent évapore la sueur #}
{% if humidex > 27 %}
  {% set effet_vent = -0.15 * vent %}
{% else %}
  {% set effet_vent = -0.25 * vent %}
{% endif %}
{# Limitation du rafraîchissement maximal par le vent à -6°C #}
{% set effet_vent = [effet_vent, -6] | max %}

{# 2. CALCUL DE L'EFFET DYNAMIQUE DU SOLEIL (VERSION SINUS) #}
{# Le soleil ne commence à chauffer la peau qu'au-dessus de 5° d'élévation #}
{% if elevation > 5 and solar > 50 %}
  {# Conversion de l'angle en radians pour la fonction sin() de HA #}
  {% set elevation_rad = elevation * 3.14159265359 / 180 %}
  {% set facteur_angle = sin(elevation_rad) %}
  
  {# Application directe du facteur sinus sur l'apport solaire #}
  {% set effet_soleil = ((solar / 300) * facteur_angle) | round(1) %}
  {# Limitation du réchauffement maximal direct à +3.5°C #}
  {% set effet_soleil = [effet_soleil, 3.5] | min %}
{% else %}
  {% set effet_soleil = 0 %}
{% endif %}

{# 3. RESSENTI RÉEL FINAL #}
{{ (humidex + effet_vent + effet_soleil) | round(1) }}

Pour modéliser cela, nous utilisons la fonction trigonométrique Sinus appliquée à l’élévation solaire.

📊 Étape 2 : Sublimer l'affichage avec Custom Button Card

Pour afficher cette donnée complexe de manière ultra-visuelle sur votre tableau de bord, voici la configuration d’une carte custom:button-card. Elle intègre une barre de progression colorée dynamique ainsi qu’un curseur noir qui se déplace en temps réel selon le niveau d’inconfort.
📋
Button card.yml
Copy to clipboard
- type: custom:button-card
  entity: sensor.ressenti_reel_combine
  name: Ressenti Réel (Vent/Soleil)
  icon: mdi:sun-thermometer
  show_state: false
  show_label: false
  styles:
    grid:
      - grid-template-areas: |
          "n n index"   
          "i . label"   
          "i bar bar"       
          "i level level" 
      - grid-template-columns: 60px 1fr 1fr
      - grid-template-rows: min-content 1fr min-content 0px
    card:
      - padding: 10px 15px
      - border-radius: 20px
      - box-shadow: var(--ha-card-box-shadow, none)
    name:
      - grid-area: "n"
      - font-size: 1.1rem
      - font-weight: 500
      - align-self: start
      - justify-self: start
      - padding-bottom: 5px
    img_cell:
      - grid-area: i
      - justify-self: start
      - align-self: center
      - width: 50px
    icon:
      - width: 2.5rem
      - height: 2.5rem
      - color: |
          [[[ 
            var val = parseFloat(entity.state);
            if (val < 30) return "#4caf50";
            if (val < 35) return "#ffeb3b";
            if (val < 40) return "#f85900";
            return "#d8001d";
          ]]]
    custom_fields:
      index:
        - grid-area: index
        - justify-self: end
        - align-self: start
        - font-size: 1.8rem
        - font-weight: 700
        - line-height: 1
      label:
        - grid-area: label
        - justify-self: end
        - align-self: end
        - font-weight: 600
        - font-size: 0.9rem
        - padding-bottom: 5px
      bar:
        - grid-area: bar
        - align-self: center
        - width: 100%
        - margin-bottom: 8px
      level:
        - grid-area: level
        - height: 0px
        - align-self: center
        - width: 100%
        - z-index: 2
        - overflow: visible
        - position: relative
        - top: "-25px"
  custom_fields:
    bar: |
      [[[
        return `<div style="background: linear-gradient(90deg, #4caf50 0%, #ffeb3b 40%, #f85900 70%, #d8001d 100%); height: 12px; border-radius: 10px; width: 100%;"></div>`
      ]]]
    level: |
      [[[
        var val = parseFloat(entity.state) || 0;
        var min = 15;
        var max = 50;
        var percent = ((val - min) / (max - min)) * 100;
        percent = Math.min(Math.max(percent, 0), 100);
        return `<div style="background: black; border: 2px solid white; height: 18px; width: 18px; border-radius: 50%; position: absolute; left: ${percent}%; transform: translateX(-50%); box-shadow: 0px 2px 4px rgba(0,0,0,0.5);"></div>`;
      ]]]
    label: |
      [[[
        var val = parseFloat(entity.state) || 0;
        if (val < 30) return '<span style="color: #4caf50">Confortable</span>';
        if (val < 35) return '<span style="color: #ffeb3b">Inconfort notable</span>';
        if (val < 40) return '<span style="color: #f85900">Inconfort évident</span>';
        if (val < 45) return '<span style="color: #d8001d">Grand inconfort</span>';
        if (val < 54) return '<span style="color: #d8001d">Inconfort dangereux</span>';
        return '<span style="color: #7f0000">Coup de chaleur</span>';
      ]]]
    index: |
      [[[
        return `${Math.round(parseFloat(entity.state) * 10) / 10}°C`;
      ]]]

🪟 Étape 3 : Doit-on ouvrir ou fermer les fenêtres ? (L'Index Éco-Confort)

Une fois le ressenti de la journée calculé, nous faisons face à un piège le soir : l’Humidex reste artificiellement haut à cause de la hausse naturelle de l’humidité relative nocturne, alors que l’air se rafraîchit.

Pour y remédier, notre algorithme effectue une bascule stratégique : dès que le soleil se couche (solar <= 10), il ignore le ressenti et passe sur la température réelle. De plus, il compare l’Humidité Absolue ($HA$, la quantité d’eau physique en $g/m³$) pour s’assurer que l’air extérieur n’est pas trop lourd ou moite avant d’autoriser l’ouverture.

📋 Prérequis et capteurs nécessaires

  • sensor.temperature_moyenne_maison : Une moyenne de vos capteurs intérieurs.

  • sensor.humidite_moyenne_maison_2 : L’humidité relative moyenne intérieure.

  • sensor.gw2000a_outdoor_temperature : La température réelle extérieure sous abri.

  • sensor.thermal_comfort_humidite_absolue : L’humidité absolue extérieure calculée par l’intégration Thermal Comfort.

  • sensor.ressenti_reel_combine : Le capteur créé à l’Étape 1.

  • sensor.saison : Pour identifier la logique été/hiver.

  • binary_sensor.maison : Un groupe de capteurs de porte/fenêtre (on = ouvert, off = fermé). Indispensable pour l’extraction dynamique des fenêtres restées ouvertes.

💻 Code du Template Sensor de Pertinence

Créez un second Template Sensor nommé sensor.pertinence_ouverture_fenetres :

📋
Template.yml
Copy to clipboard
{% set temp_int = states('sensor.temperature_moyenne_maison') | float(0) | round(1) %}
{% set hum_int = states('sensor.humidite_moyenne_maison_2') | float(0) %}
{% set ha_int = ((6.112 * 2.718281828 ** ((17.67 * temp_int) / (temp_int + 243.5))) * hum_int * 2.1674 / (273.15 + temp_int)) | round(1) %}

{% set temp_ext_reelle = states('sensor.gw2000a_outdoor_temperature') | float(0) | round(1) %}
{% set ha_ext = states('sensor.thermal_comfort_humidite_absolue') | float(0) | round(1) %}
{% set solar = states('sensor.gw2000a_solar_radiation') | float(0) %}

{# --- CORRECTION STRATÉGIQUE NUIT --- #}
{# Si le soleil est couché ou rayonnement nul, on utilise la température réelle, sinon le ressenti lourd #}
{% if solar > 10 %}
  {% set temp_ext_efficace = states('sensor.ressenti_reel_combine') | float(0) | round(1) %}
{% else %}
  {% set temp_ext_efficace = temp_ext_reelle %}
{% endif %}

{% set saison = states('sensor.saison') %}
{% set mode_ete = saison in ['summer', 'spring'] %}
{% set maison_ouverte = is_state('binary_sensor.maison', 'on') %}

{% set rec = 'NEUTRE' %}
{% set motif = '' %}

{# --- STRATÉGIE ÉTÉ / PRINTEMPS --- #}
{% if mode_ete %}
  {% if temp_ext_efficace < temp_int - 1.0 and ha_ext < ha_int + 1.2 %}
    {% set rec = 'OUVRIR' %}
    {% set motif = 'Il fait plus frais dehors (' ~ temp_ext_efficace ~ '°C vs ' ~ temp_int ~ '°C)' %}
  {% elif temp_ext_efficace > temp_int + 1.5 %}
    {% set rec = 'FERMER' %}
    {% set motif = 'Dehors est plus chaud (' ~ temp_ext_efficace ~ '°C vs ' ~ temp_int ~ '°C)' %}
  {% elif ha_ext > ha_int + 1.5 %}
    {% set rec = 'FERMER' %}
    {% set motif = 'Dehors est trop lourd/humide (' ~ ha_ext ~ 'g/m³)' %}
  {% else %}
    {% set motif = 'Conditions similaires' %}
  {% endif %}
{% else %}
  {# --- STRATÉGIE HIVER / AUTOMNE --- #}
  {% if temp_ext_efficace > temp_int + 1.5 %}
    {% set rec = 'OUVRIR' %}
    {% set motif = 'Le soleil réchauffe dehors (' ~ temp_ext_efficace ~ '°C vs ' ~ temp_int ~ '°C)' %}
  {% elif hum_int > 60 and ha_ext < ha_int - 0.8 %}
    {% set rec = 'OUVRIR' %}
    {% set motif = "Chasser l'humidité intérieure" %}
  {% elif temp_ext_efficace < temp_int - 1.5 %}
    {% set rec = 'FERMER' %}
    {% set motif = 'Il fait plus froid dehors (' ~ temp_ext_efficace ~ '°C vs ' ~ temp_int ~ '°C)' %}
  {% else %}
    {% set motif = 'Températures similaires' %}
  {% endif %}
{% endif %}

{# --- LOGIQUE DYNAMIQUE DES OUVERTURES --- #}
{% if rec == 'FERMER' %}
  {% if maison_ouverte %}
    {% set ouvertes = state_attr('binary_sensor.maison', 'entity_id') | select('is_state', 'on') | map('state_attr', 'friendly_name') | join(', ') %}
    🔴 Action requise : Fermer [{{ ouvertes }}] | {{ motif }}
  {% else %}
    🟢 Situation optimale | Maison protégée : {{ motif }}
  {% endif %}
{% elif rec == 'OUVRIR' %}
  {% if maison_ouverte %}
    🟢 Situation optimale | Aération en cours : {{ motif }}
  {% else %}
    🟡 Opportunité | Ouvrir les fenêtres : {{ motif }}
  {% endif %}
{% else %}
  🟢 Statut Neutre | {{ motif }}
{% endif %}

🎨 Étape 4 : Intégration sur le Tableau de bord (Markdown & Mushroom)

Pour exploiter ce capteur de pertinence, voici deux méthodes d’intégration sur votre interface Lovelace. Le code découpe automatiquement le statut pour isoler l’action principale du motif technique.

Markdown

📋
Markdown.yml
Copy to clipboard
type: markdown
content: |-
  # Accueil
  {% set pertinence = states('sensor.pertinence_ouverture_fenetres') %}
  {% set action = pertinence.split('|')[0] | trim if '|' in pertinence else pertinence | trim %}
  {% set raison = pertinence.split('|')[1] | trim if '|' in pertinence else '' %}

  **🍃 Confort Thermique
  {{ action }}** {% if raison %}: _{{ raison }}_{% endif %}

Carte Mushroom Template

📋
mushroom-template-card.yml
Copy to clipboard
- type: custom:mushroom-template-card
  entity: sensor.pertinence_ouverture_fenetres
  primary: >-
    {% set p = states('sensor.pertinence_ouverture_fenetres').split('|')[0] %}
    {{ p | replace('🔴', '') | replace('🟢', '') | replace('🟡', '') | trim }}
  secondary: >-
    {{ states('sensor.pertinence_ouverture_fenetres').split('|')[1] | trim if '|' in states('sensor.pertinence_ouverture_fenetres') else '' }}
  icon: >-
    {% set s = states('sensor.pertinence_ouverture_fenetres') %} 
    {% if 'Ouvrir' in s or 'Aération' in s %} mdi:window-open-variant
    {% else %} mdi:window-closed-variant {% endif %}
  color: >-
    {% set s = states('sensor.pertinence_ouverture_fenetres') %} 
    {% if '🔴' in s %} red 
    {% elif '🟡' in s %} orange 
    {% elif '🟢' in s %} green 
    {% else %} grey {% endif %}
  multiline_secondary: true
  vertical: true

✨ Conclusion

En couplant la puissance mathématique de Home Assistant à des cartes graphiques avancées, vous passez d’une domotique passive à une domotique contextuelle et intelligente. Votre maison sait désormais analyser l’impact du vent et l’angle du soleil pour vous guider au quotidien vers d’importantes économies d’énergie !