Indices météo sous Home Assistant : rosée, givre et humidex

Domotique 11 min de lecture

Point de rosée, humidité absolue, givre, indice de chaleur et humidex : voici comment je calcule mes indices météo dans Home Assistant.

Parapluies
Image de Sharon Garner

N'ayant pas encore de station météo digne de ce nom, j'utilise trois sources distinctes pour connaitre quelques données météorologiques chez nous.

Capteur de température Aqara dans un boitier pour le prétéger des imtempéries de l'extérieur
Mon capteur de température Aqara dans son boitier prêt à affronter l'extérieur

ℹ️ Je privilégie autant que possible les sources de données locales plutôt que celles hébergées dans le cloud.

⚠️ Cet article a été publié initialement en 2022 et certains exemples ne sont plus fonctionnels en l'état. L'attribut forecast des entités météo a été supprimé avec la version 2024.4 au profit de l'action weather.get_forecasts : le capteur de température maximale de la section Bonus, ainsi que le binary sensor de forte chaleur qui en dépend, ne fonctionnent plus. Par ailleurs, la syntaxe des automatisations a changé (service est devenu action avec la version 2024.8) et Home Assistant impose désormais des unités strictes selon la classe d'appareil, ce qui affecte le capteur d'humidité absolue ci-dessous. Le principe général reste valable, mais il vous faudra adapter les exemples à votre version.

Il existe une intégration disponible sur HACS, Thermal Comfort, qui fournit une partie des éléments présentés dans cet article, mais j'ai préféré le faire moi-même pour mieux comprendre chaque donnée, calcul ou indice.

Le point de rosée

Le point de rosée est également appelé « température de rosée ». Il s'agit tout simplement de la température en degrés en dessous de laquelle la rosée se dépose de façon naturelle à l'extérieur.

template:
  - sensor:
      # https://en.wikipedia.org/wiki/Dew_point#Calculating_the_dew_point
      name: "Dew Point"
      unit_of_measurement: "°C"
      device_class: temperature
      state_class: measurement
      state: >-
        {% set h = states('sensor.garden_sensors_humidity') | float(1.0) %} {## ⚠️ À modifier ##}
        {% set t = states('sensor.garden_sensors_temperature') | float(1.0) %} {## ⚠️ À modifier ##}

        {% set b = 17.62 %}
        {% set c = 243.12 %}
        {% set l = log(h / 100, e, 1.0) + ((b * t) / (c + t)) %}

        {{ ((c * l) / (b - l)) | round(2) }}
      availability: >-
        {{ is_number(states('sensor.garden_sensors_humidity')) and
          is_number(states('sensor.garden_sensors_temperature')) }} {## ⚠️ À modifier ##}

L'humidité absolue

L'humidité absolue décrit la quantité totale d'eau contenue dans un certain volume d'air (g/m³).

template:
  - sensor:
      # https://pon.fr/dzvents-alerte-givre-et-calcul-humidite-absolue/
      name: "Absolute Humidity"
      unit_of_measurement: "g/m³"
      device_class: humidity
      state_class: measurement
      state: >-
        {% set h = states('sensor.garden_sensors_humidity') | float(1.0) %} {## ⚠️ À modifier ##}
        {% set t = states('sensor.garden_sensors_temperature') | float(1.0) %} {## ⚠️ À modifier ##}

        {{ ((6.112 * (e**((17.67 * t) / (t + 243.5))) * h * 2.1674) / (273.15 + t)) | round(2) }}
      availability: >-
        {{ is_number(states('sensor.garden_sensors_humidity')) and
          is_number(states('sensor.garden_sensors_temperature')) }} {## ⚠️ À modifier ##}

Le point de givrage

Le point de givrage est la température en dessous de laquelle l'air n'a plus assez d'énergie pour maintenir l'eau qu'il contient sous forme de vapeur : elle se solidifie et se transforme en cristaux.

Indice de risque :

  • 0 : Aucun risque de givrage
  • 1 : Givre peu probable malgré la température
  • 2 : Givre probable malgré la température
  • 3 : Forte probabilité de givre
template:
  - sensor:
      # https://pon.fr/dzvents-alerte-givre-et-calcul-humidite-absolue/
      name: "Frost Point"
      unit_of_measurement: "°C"
      device_class: temperature
      state_class: measurement
      state: >-
        {% set t = (states('sensor.garden_sensors_temperature') | float(1.0)) + 273.15 %} {## ⚠️ À modifier ##}
        {% set td = (states('sensor.dew_point') | float(1.0)) + 273.15 %}

        {{ ((td + (2671.02 / ((2954.61 / t) + (2.193665 * log(t, e, 1.0)) - 13.3448)) - t) - 273.15) | round(2) }}
      attributes:
        risk: >-
          {% set f = states('sensor.frost_point') | float(1.0) %}
          {% set a = states('sensor.absolute_humidity') | float(1.0) %}
          {% set t = states('sensor.garden_sensors_temperature') | float(1.0) %} {## ⚠️ À modifier ##}

          {% if t <= 1 and f <= 0 %}
            {% if a <= 2.8 %}
              1
            {% else %}
              3
            {% endif %}
          {% elif t <= 4 and f <= 0.5 and a > 2.8 %}
            2
          {% else %}
            0
          {% endif %}
      availability: >
        {{ is_number(states('sensor.garden_sensors_temperature')) and
          is_number(states('sensor.absolute_humidity')) and
          is_number(states('sensor.dew_point')) }} {## ⚠️ À modifier ##}

L'indice de chaleur

L'indice de chaleur indique une température ressentie par le corps humain à l'ombre, en fonction de l'humidité relative et de la température de l'air. Il n'est valable qu'à partir d'une température de l'air de 27 °C, d'un point de rosée de 12 °C et d'une humidité relative de 40 %.

Indice de risque :

  • 0 : Aucun inconfort
  • 1 : Inconfort
  • 2 : Extrême inconfort
  • 3 : Danger
  • 4 : Danger extrême
template:
  - sensor:
      # https://en.wikipedia.org/wiki/Heat_index
      name: "Heat Index"
      unit_of_measurement: "°C"
      device_class: temperature
      state_class: measurement
      state: >-
        {% set t = states('sensor.garden_sensors_temperature') | float(0.0) %} {## ⚠️ À modifier ##}
        {% set r = states('sensor.garden_sensors_humidity') | float(0.0) %} {## ⚠️ À modifier ##}

        {% set p1 = -8.78469475556 %}
        {% set p2 = 1.61139411 * t %}
        {% set p3 = 2.33854883889 * r %}
        {% set p4 = -0.14611605 * t * r %}
        {% set p5 = -0.012308094 * t * t %}
        {% set p6 = -0.0164248277778 * r * r %}
        {% set p7 = (2.211732e-3) * t * t * r %}
        {% set p8 = (7.2546e-4) * t * r * r %}
        {% set p9 = (3.582e-6) * t * t * r * r %}

        {{ (p1 + p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9) | round(0) }}
      attributes:
        risk: >-
          {% if (states('sensor.garden_sensors_temperature') | float(0.0)) >= 27 and (states('sensor.garden_sensors_humidity') | float(0.0)) >= 40 and (states('sensor.dew_point') | float(0.0)) >= 12 %} {## ⚠️ À modifier ##}
            {% set i = states('sensor.heat_index') | float(0.0) %}

            {% if i >= 54 %}
              4
            {% elif i >= 41 %}
              3
            {% elif i >= 32 %}
              2
            {% elif i >= 27 %}
              1
            {% else %}
              0
            {% endif %}
          {% else %}
            0
          {% endif %}
      icon: >-
        {% set r = state_attr('sensor.heat_index', 'risk') | int(0) %}

        {% if r == 4 %}
          mdi:alert-octagon
        {% elif r == 3 %}
          mdi:alert
        {% elif r == 2 %}
          mdi:alert-outline
        {% elif r == 1 %}
          mdi:information-outline
        {% else %}
          mdi:check-circle-outline
        {% endif %}

L'indice humidex

L'indice humidex prend en compte la température de l'air et le point de rosée pour mesurer le confort ressenti par un humain vis-à-vis de la chaleur.

template:
  - sensor:
      # https://fr.wikipedia.org/wiki/Indice_humidex
      name: "Humidex"
      state: >-
        {% set t = states('sensor.garden_sensors_temperature') | float(0.0) %} {## ⚠️ À modifier ##}
        {% set r = states('sensor.dew_point') | float(0.0) %}

        {% set a = 5417.7530*((1 / 273.16) - (1 / (273.15 + r))) %}

        {{ (t + 0.55555 * (6.11 * (e ** a) - 10)) | round(0) }}
      attributes:
        risk: >-
          {% set h = states('sensor.humidex') | float(0.0) %}

          {% if h >= 54 %}
            4
          {% elif h >= 45 %}
            3
          {% elif h >= 40 %}
            2
          {% elif h >= 30 %}
            1
          {% else %}
            0
          {% endif %}
      icon: >-
        {% set r = state_attr('sensor.humidex', 'risk') | int(0) %}

        {% if r == 4 %}
          mdi:alert-octagon
        {% elif r == 3 %}
          mdi:alert
        {% elif r == 2 %}
          mdi:alert-outline
        {% elif r == 1 %}
          mdi:information-outline
        {% else %}
          mdi:check-circle-outline
        {% endif %}

Bonus

Anticiper les grosses chaleurs

Pour anticiper les périodes de chaleur, j'ai besoin de connaitre la température maximale actuelle et celles des 3 prochains jours. Pour cela, je m'appuie sur les données du capteur de température Aqara du jardin et sur celles de Météo-France.

⚠️ N'oubliez pas de remplacer les entités sensor.garden_sensors_temperature et weather.xxx par les vôtres !

template:
  - sensor:
      name: "Outside - Temperature max - Next days"
      unit_of_measurement: "°C"
      device_class: temperature
      state_class: measurement
      state: >-
        {% set sensors = [
          states('sensor.garden_sensors_temperature') | float(0.0),
          state_attr('weather.xxx', 'forecast')[0].temperature | float(0.0),
          state_attr('weather.xxx', 'forecast')[1].temperature | float(0.0),
          state_attr('weather.xxx', 'forecast')[2].temperature | float(0.0),
          state_attr('weather.xxx', 'forecast')[3].temperature | float(0.0),
        ] %}

        {{ sensors | max }}

On ajoute un input_number pour régler notre seuil limite de fortes chaleurs.

input_number:
  outside_heatwave:
    name: Seuil - Forte chaleur
    min: "0"
    max: "50"
    step: "1"
    unit_of_measurement: "°C"

Et enfin un binary_sensor pour savoir si nous sommes actuellement, ou serons dans les trois prochains jours, en période de fortes chaleurs :

template:
  - binary_sensor:
      name: "Outside - Is heatwave"
      state: "{{ states('sensor.outside_temperature_max_next_days') | float(0) >= states('input_number.outside_heatwave') | float(0) }}"
      device_class: heat

Ensoleillement

Pour déterminer si le soleil tape sur la façade, j'utilise le capteur de luminosité Xiaomi YTC4043GL.

Capteur de liminosité Xiaomi situé sous le coffre du volet roulant pour le protéger de la pluie
Capteur de liminosité Xiaomi situé sous le coffre du volet roulant pour le protéger de la pluie

⚠️ Le Xiaomi YTC4043GL n'est pas étanche, il faut le protéger au maximum de la pluie !

Le capteur me donne la luminosité à un instant T et non une moyenne, ce qui peut être gênant lorsqu'un nuage passe devant le soleil et fait chuter la valeur. Pour lisser cela, nous allons utiliser l'intégration statistics afin de calculer la moyenne sur les 15 dernières minutes, ainsi qu'un sensor pour reprendre la valeur brute du capteur lorsque la moyenne calculée est unavailable :

⚠️ N'oubliez pas de remplacer outside_south_illuminance_lux par votre capteur de luminosité

sensor:
  - platform: statistics
    name: "Outside - Illuminance (lux) - South - Stats"
    entity_id: sensor.outside_south_illuminance_lux
    state_characteristic: average_linear
    max_age: "00:15"
    sampling_size: 500
    precision: 0

template:
  - sensor:
      - name: "Outside - Illuminance (lux) - South"
        unit_of_measurement: "lx"
        device_class: illuminance
        state_class: measurement
        state: "{{ states('sensor.outside_illuminance_lux_south_stats') if is_number(states('sensor.outside_illuminance_lux_south_stats')) else states('sensor.outside_south_illuminance_lux') }}"

Pour éviter qu'au seuil mesuré le booléen oscille entre true et false, on va utiliser le principe du cycle d'hystérésis :

  • Si la valeur dépasse (seuil + marge) lux, on considère qu'il y a ensoleillement (booléen à true)
  • Si la valeur est inférieure à (seuil - marge) lux, on considère qu'il n'y a plus d'ensoleillement (booléen à false)
  • Entre (seuil - marge) et (seuil + marge) lux, on garde la valeur précédente

Nous allons utiliser deux input_number pour les seuils, une automatisation pour gérer le cycle d'hystérésis et un input_boolean pour stocker le résultat :

⚠️ Le seuil haut (seuil + marge) doit toujours être supérieur au seuil bas (seuil - marge). Par simplification, il n'y a aucune vérification dans l'automatisation !

input_boolean:
  sunny_south:
    name: Ensoleillement - Sud

input_number:
  sunny_south_low_threshold:
    name: Ensoleillement - Sud - Seuil bas
    min: "0"
    max: "83000"
    step: "1"
    unit_of_measurement: "lux"
  sunny_south_high_threshold:
    name: Ensoleillement - Sud - Seuil haut
    min: "0"
    max: "83000"
    step: "1"
    unit_of_measurement: "lux"

automation:
  - description: Sunny - South - Threshold
    alias: sunny_south_threshold
    id: d5637a4f-08b6-4ff2-88ba-77b7626a0513
    mode: restart
    trigger:
      - platform: state
        entity_id: input_boolean.sunny_south
      - platform: state
        entity_id: sensor.outside_illuminance_lux_south
    action:
      - choose:
          - conditions:
              - condition: template
                value_template: "{{ not is_state('input_boolean.sunny_south', 'on') }}"
              - condition: numeric_state
                entity_id: sensor.outside_illuminance_lux_south
                above: input_number.sunny_south_high_threshold
            sequence:
              - service: input_boolean.turn_on
                target:
                  entity_id: input_boolean.sunny_south
          - conditions:
              - condition: template
                value_template: "{{ not is_state('input_boolean.sunny_south', 'off') }}"
              - condition: numeric_state
                entity_id: sensor.outside_illuminance_lux_south
                below: input_number.sunny_south_low_threshold
            sequence:
              - service: input_boolean.turn_off
                target:
                  entity_id: input_boolean.sunny_south

Il ne vous reste plus qu'à dupliquer cet ensemble pour chacune de vos façades, en remplaçant south par l'orientation concernée.

Lovelace

Affichage des champs sur le dashboard Lovelace
Affichage des champs sur le dashboard Lovelace

J'utilise trois cartes personnalisées sous Lovelace pour réaliser cette interface :

type: vertical-stack
title: Jardin
cards:
  - type: entities
    entities:
      - entity: sensor.garden_sensors_temperature
        type: custom:multiple-entity-row
        name: Informations
        state_header: Température
        secondary_info: last-changed
        entities:
          - sensor.garden_sensors_humidity
      - entity: sensor.outside_illuminance_lux_south
        type: custom:multiple-entity-row
        name: Luminosité
        icon: mdi:brightness-5
        state_header: Sud
        secondary_info: last-changed
        entities:
          - entity: sensor.outside_illuminance_lux_east
            name: Est
      - entity: input_boolean.sunny_south
        type: custom:multiple-entity-row
        name: Ensoleillement
        icon: mdi:weather-sunny
        state_header: Sud
        secondary_info: last-changed
        entities:
          - entity: input_boolean.sunny_east
            name: Est
      - entity: sensor.frost_point
        attribute: risk
        type: custom:multiple-entity-row
        name: Point de givrage
        state_header: Risque
        secondary_info: last-changed
      - entity: binary_sensor.outside_is_heatwave
        type: custom:multiple-entity-row
        name: Forte chaleur
        secondary_info: last-changed
      - entity: sensor.humidex
        attribute: risk
        type: custom:multiple-entity-row
        name: Humidex
        state_header: Risque
        secondary_info: last-changed
      - entity: sensor.heat_index
        attribute: risk
        type: custom:multiple-entity-row
        name: Indice de chaleur
        state_header: Risque
        secondary_info: last-changed
      - type: custom:fold-entity-row
        head:
          label: PARAMÉTRAGE
          type: section
        entities:
          - entity: input_number.outside_heatwave
            type: custom:numberbox-card
            icon: mdi:thermometer
            icon_plus: mdi:chevron-up
            icon_minus: mdi:chevron-down
          - entity: input_number.sunny_east_high_threshold
            type: custom:numberbox-card
            icon: mdi:eye-plus
            icon_plus: mdi:chevron-up
            icon_minus: mdi:chevron-down
          - entity: input_number.sunny_east_low_threshold
            type: custom:numberbox-card
            icon: mdi:eye-minus
            icon_plus: mdi:chevron-up
            icon_minus: mdi:chevron-down
          - entity: input_number.sunny_south_high_threshold
            type: custom:numberbox-card
            icon: mdi:eye-plus
            icon_plus: mdi:chevron-up
            icon_minus: mdi:chevron-down
          - entity: input_number.sunny_south_low_threshold
            type: custom:numberbox-card
            icon: mdi:eye-minus
            icon_plus: mdi:chevron-up
            icon_minus: mdi:chevron-down

Conclusion

Cet article est un peu long, car j'avais besoin de vous présenter les différentes données, calculs et indices météorologiques que j'utilise. Ils nous resserviront dans le prochain article sur la gestion des volets.

Vous pouvez aller plus loin en ajoutant, par exemple, des alertes sur les différents risques (givre, chaleur).