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.
Pour reproduire ce tutoriel, vous aurez besoin de données météo précises.
La configuration idéale (Station locale)
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 :

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.
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).
Créez un nouveau Template Sensor (via l’interface ou votre fichier configuration.yaml) et collez ce code :
{% 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.

- 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`;
]]]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.
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.
Créez un second Template Sensor nommé sensor.pertinence_ouverture_fenetres :
{% 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 %}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.

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 %}
- 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