Re,
Le diffuseur contextuel de l’article passe en V2 ici, avec @gael on a pas mal cherché de solutions pour surveiller les ouvrants ouverts et qui par oublie ou inattention, restent ouverts. J’ai fini par trouver une méthode simple en ajoutant 2 helper de surveillance des ouvrants extérieurs ( une seconde version verra le jour pour la surveillance des congélo et autres frigo qui nécessitent un temps de réaction plus court. mais posons les bases).
j’ai donc commencé par ajouter 2 helper liste et nombre d’ouvrants ( que je liste, parce que c’est plus simple à maintenir comme ça pour moi. Tout ça pour ajouter un bouton qui me signale les ouvrants ouverts et leur nombre.
Liste_ouvrants
{{ expand([
'binary_sensor.porte1_salon_2',
'binary_sensor.porte2_salon_2',
'binary_sensor.porte_entree_2',
'binary_sensor.olimex_esp32_poe_veranda_fenetre1_veranda',
'binary_sensor.olimex_esp32_poe_veranda_fenetre2_veranda',
'binary_sensor.olimex_esp32_poe_veranda_fenetre3_veranda',
'binary_sensor.olimex_esp32_poe_veranda_porte1_veranda',
'binary_sensor.olimex_esp32_poe_veranda_porte2_veranda',
'binary_sensor.olimex_esp32_poe_veranda_fenetre4_veranda',
'binary_sensor.olimex_esp32_poe_veranda_fenetre5_veranda',
'binary_sensor.olimex_esp32_poe_veranda_fenetre6_veranda',
'binary_sensor.olimex_esp32_poe_garagext_porte1_garagext',
'binary_sensor.olimex_esp32_poe_garagext_porte2_garagext',
'binary_sensor.olimex_esp32_poe_garagext_porte3_garagext',
'binary_sensor.olimex_esp32_poe_garagext_porte4_garagext',
'binary_sensor.olimex_esp32_poe_garagext_fenetre1_garagext',
'binary_sensor.olimex_esp32_poe_garagext_fenetre2_garagext',
'binary_sensor.olimex_esp32_poe_garagext_fenetre3_garagext',
'binary_sensor.olimex_esp32_poe_garagext_fenetre4_garagext',
'binary_sensor.olimex_esp32_poe_etage_fd_amis',
'binary_sensor.olimex_esp32_poe_etage_fg_amis',
'binary_sensor.olimex_esp32_poe_etage_fg_sdb',
'binary_sensor.olimex_esp32_poe_etage_fd_sdb',
'binary_sensor.olimex_esp32_poe_etage_fd_laurent',
'binary_sensor.olimex_esp32_poe_etage_fg_laurent',
'binary_sensor.olimex_esp32_poe_etage_fg_papa',
'binary_sensor.olimex_esp32_poe_etage_fd_papa'
]) | selectattr('state', 'eq', 'on') | map(attribute='name') | list | join(', ') }}
nombre_ouvrants
{{ [
states('binary_sensor.porte1_salon_2'),
states('binary_sensor.porte2_salon_2'),
states('binary_sensor.porte_entree_2'),
states('binary_sensor.olimex_esp32_poe_veranda_fenetre1_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_fenetre2_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_fenetre3_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_porte1_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_porte2_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_fenetre4_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_fenetre5_veranda'),
states('binary_sensor.olimex_esp32_poe_veranda_fenetre6_veranda'),
states('binary_sensor.olimex_esp32_poe_garagext_porte1_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_porte2_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_porte3_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_porte4_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_fenetre1_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_fenetre2_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_fenetre3_garagext'),
states('binary_sensor.olimex_esp32_poe_garagext_fenetre4_garagext'),
states('binary_sensor.olimex_esp32_poe_etage_fd_amis'),
states('binary_sensor.olimex_esp32_poe_etage_fg_amis'),
states('binary_sensor.olimex_esp32_poe_etage_fg_sdb'),
states('binary_sensor.olimex_esp32_poe_etage_fd_sdb'),
states('binary_sensor.olimex_esp32_poe_etage_fd_laurent'),
states('binary_sensor.olimex_esp32_poe_etage_fg_laurent'),
states('binary_sensor.olimex_esp32_poe_etage_fg_papa'),
states('binary_sensor.olimex_esp32_poe_etage_fd_papa')
] | select('eq', 'on') | list | count }}
Et là… le déclic

On obtient une liste, pourquoi ne pas la diffuser tant qu’un de ces ouvrants restent ouverts?
automation de surveillance de portes restées ouvertes
alias: TTS - Alerte Ouvrant reste ouvert 5 min
description: >-
Envoie un message initial après 5 min, puis le répète toutes les 5 min tant
qu'un ouvrant est ouvert.
triggers:
- entity_id: sensor.nombre_ouvrants
above: 0
for: "00:05:00"
trigger: numeric_state
conditions:
- condition: numeric_state
entity_id: sensor.nombre_ouvrants
above: 0
actions:
- repeat:
while:
- condition: numeric_state
entity_id: sensor.nombre_ouvrants
above: 0
sequence:
- target:
entity_id: automation.tts_diffuseur_contextuel
data:
skip_condition: true
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : {{
states('sensor.liste_ouvrants') }}
action: automation.trigger
- delay: "00:05:00"
mode: single
I. Section « Triggers » (Déclencheurs) 
Le but ici est d’attendre 5 minutes complètes avant de lancer la séquence d’alerte.
triggers:
- entity_id: sensor.nombre_ouvrants
above: 0
for: "00:05:00"
trigger: numeric_state
entity_id: sensor.nombre_ouvrants: Cible votre Helper qui compte le nombre d’ouvrants ouverts.
above: 0: La condition est remplie si le nombre d’ouvrants ouverts est strictement supérieur à zéro (c’est-à-dire qu’au moins un ouvrant est ouvert).
for: "00:05:00": C’est le point clé. L’automatisation ne se déclenchera qu’une seule fois : au moment où l’entité sensor.nombre_ouvrants a maintenu une valeur > 0 pendant 5 minutes consécutives.
II. Section « Conditions » (Vérification) 
Cette section est une vérification de sécurité qui garantit que l’alerte n’est pas lancée si la porte vient d’être fermée juste après le délai de 5 minutes.
conditions:
- condition: numeric_state
entity_id: sensor.nombre_ouvrants
above: 0
- On vérifie une dernière fois que le nombre d’ouvrants ouverts est toujours
> 0 au moment où l’action va se lancer. Si une porte a été fermée à T+4:59, le déclencheur n’aurait pas fonctionné. Si la porte est fermée à T+5:01, cette condition empêche l’action.
III. Section « Actions » (La Boucle Répétitive) 
C’est ici que la logique de répétition conditionnelle est mise en place grâce au bloc repeat.
actions:
- repeat:
while:
- condition: numeric_state
entity_id: sensor.nombre_ouvrants
above: 0
sequence:
# ... (Envoi du TTS)
- delay: "00:05:00"
1. repeat: while (La Condition de Maintien)
while: Ce bloc définit la condition qui doit être vraie pour que la séquence d’actions (sequence) soit exécutée à nouveau.
condition: numeric_state / above: 0: Tant que le sensor.nombre_ouvrants est supérieur à zéro (donc au moins un ouvrant est ouvert), la boucle continue. Si cette condition devient fausse (tous les ouvrants sont fermés), la boucle s’arrête immédiatement.
2. sequence (Les Étapes de l’Alerte)
- target:
entity_id: automation.tts_diffuseur_contextuel
data:
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : {{ states('sensor.liste_ouvrants') }}
action: automation.trigger
- On appelle le diffuseur contextuel ( on va y revenir, c’est le coeur du système )
TTS - Diffuseur contextuel en lui passant un message. Le message utilise le Helper de liste (sensor.liste_ouvrants) pour indiquer précisément quelles portes sont ouvertes.
- Délai entre les Alertes:
- delay: "00:05:00"
Après avoir diffusé le message, l’automatisation marque une pause de 5 minutes. Une fois cette pause terminée, l’automatisation revient automatiquement vérifier la condition while pour décider de répéter l’alerte ou de s’arrêter.
IV. Section « Mode » (Gestion de l’Instance) 
mode: single
mode: single: Ce mode garantit que l’automatisation ne peut pas se redéclencher tant que la première instance est en cours d’exécution (c’est-à-dire tant qu’elle est dans le delay de la boucle repeat). C’est le mode idéal ici, car il garantit que vous ne lancez qu’une seule boucle d’alerte, même si d’autres portes s’ouvrent pendant le délai. (à voir suivant votre utilisation).
On en vient au coeur du système détaillé dans l’article, le diffuseur contextuel.
Diffuseur contextuel V1
alias: TTS - Diffuseur contextuel
description: Diffuse un message dans les pièces où une présence est détectée
triggers:
- event_type: dummy_startup_never_fired
trigger: event
conditions:
- condition: template
value_template: "{{ occupied_rooms | length > 0 }}"
- condition: template
value_template: "{{ door_message is defined and door_message | length > 0 }}"
actions:
- target:
entity_id: "{{ occupied_rooms }}"
data:
volume_level: 0.8
action: media_player.volume_set
- delay: "00:00:01"
- target:
entity_id: tts.google_en_com
data:
cache: true
media_player_entity_id: "{{ occupied_rooms }}"
message: "{{ message }}"
language: fr
action: tts.speak
- wait_template: "{{ is_state(occupied_rooms[0], 'playing') }}"
timeout: "00:00:01"
- wait_template: "{{ is_state(occupied_rooms[0], 'idle') }}"
timeout: "00:00:05"
- target:
entity_id: "{{ occupied_rooms }}"
data:
volume_level: 0.5
action: media_player.volume_set
mode: queued
variables:
message: "{{ trigger.variables.message | default('') }}"
occupied_rooms: >
{% set rooms = [] %} {% if states('sensor.presence_entree') == 'Entree' %}{%
set rooms = rooms + ['media_player.entree'] %}{% endif %} {% if
states('sensor.presence_cagibi') == 'Cagibi' %}{% set rooms = rooms +
['media_player.entree'] %}{% endif %} {% if
states('sensor.presence_cuisine') == 'Cuisine' %}{% set rooms = rooms +
['media_player.cuisine'] %}{% endif %} {% if
states('sensor.presence_salle_manger') == 'Salle_manger' %}{% set rooms =
rooms + ['media_player.salon'] %}{% endif %} {% if
states('sensor.presence_chaufferie') == 'Chaufferie' %}{% set rooms = rooms
+ ['media_player.chaufferie'] %}{% endif %} {% if
states('sensor.presence_labo') == 'Labo' %}{% set rooms = rooms +
['media_player.labo'] %}{% endif %} {% if
states('sensor.presence_garage_interne') == 'Garage_interne' %}{% set rooms
= rooms + ['media_player.garage_interne'] %}{% endif %} {% if
states('sensor.presence_veranda') == 'Veranda' %}{% set rooms = rooms +
['media_player.veranda'] %}{% endif %} {% if states('sensor.presence_wc') ==
'Wc' %}{% set rooms = rooms + ['media_player.toilettes'] %}{% endif %} {% if
states('sensor.presence_chambre_laurent') == 'Chambre_laurent' %}{% set
rooms = rooms + ['media_player.chambre_laurent'] %}{% endif %} {% if
states('sensor.presence_chambre_papa') == 'Chambre_papa' %}{% set rooms =
rooms + ['media_player.chambre_papa'] %}{% endif %} {% if
states('sensor.presence_chambre_amis') == 'Chambre_amis' %}{% set rooms =
rooms + ['media_player.chambre_amis'] %}{% endif %} {% if
states('sensor.presence_sdb') == 'Sdb' %}{% set rooms = rooms +
['media_player.salle_de_bain'] %}{% endif %} {% if
states('sensor.presence_garage_externe') == 'Garage_externe' %}{% set rooms
= rooms + ['media_player.garage_externe'] %}{% endif %} {{ rooms | unique |
list }}
j’avais quand même un soucis de taille, autant pour le reste tout fonctionne, autant pour la surveillance de ouvrants et je pense que c’est un soucis de longueur de message, le diffuseur montait bien le volume, puis le baissait avant de le diffuser sur la/les borne(s) de la ou des pièces occupées.
sur une idée originale de @Gael, on passe donc de ça
- wait_template: "{{ is_state(occupied_rooms[0], 'playing') }}"
timeout: "00:00:01"
- wait_template: "{{ is_state(occupied_rooms[0], 'idle') }}"
timeout: "00:00:05"
à ça
- variables:
calculated_delay: "{{ ((message | length / 12) + 3) | round(0) }}"
- delay:
hours: 0
minutes: 0
seconds: "{{ calculated_delay | int }}"
milliseconds: 0
La nouvelle méthode utilise la longueur du message (message | length) pour estimer de manière dynamique le temps de lecture, ajoutant une marge de sécurité.
calculated_delay: "{{ ((message | length / 12) + 3) | round(0) }}"
-
message | length / 12: On divise la longueur totale du message (nombre de caractères) par une estimation du taux de parole (environ 12 caractères par seconde). Cela donne une bonne approximation de la durée de lecture.
-
3: On ajoute 3 secondes de marge de sécurité. Ces secondes sont essentielles pour couvrir la latence du réseau, le temps de chargement du fichier TTS sur l’appareil Cast, et le temps de transition.
| round(0): On arrondit le résultat à la seconde près.
- variables:
calculated_delay: ...
- delay:
seconds: "{{ calculated_delay | int }}"
Il est essentiel d’expliquer pourquoi la méthode d’attente a été changée. La nouvelle méthode basée sur un délai calculé dynamiquement est bien plus robuste pour la diffusion TTS sur des appareils Cast (Google Home).
Pourquoi Changer le Mécanisme d’Attente ?
Le problème principal des notifications TTS (Text-to-Speech) est d’assurer que le système attend la fin complète de la lecture avant d’exécuter l’action suivante (dans notre cas, baisser le volume à 0.5).
L’ancienne méthode, utilisant wait_template sur les états playing et idle, est souvent unreliable pour les raisons suivantes :
- Problèmes de l’Ancienne Méthode (wait_template)

Problème Explication
Latence Le service TTS (Google, Amazon) prend quelques secondes pour générer le fichier audio, le charger et le lire. Le timeout de 5 secondes pour attendre l’état idle (inactivité) est trop court si le message est long (comme une liste de portes ouvertes).
États Capricieux Les appareils Cast ne changent pas toujours d’état rapidement ou de manière fiable entre playing et idle. L’état peut rester sur playing même après la fin du message ou passer trop tôt à idle.
Timeout Fixe Si un message prend 15 secondes à lire, le timeout de 5 secondes ne sera pas suffisant, et le script baissera le volume pendant la lecture, coupant le message.
- Avantages de la Nouvelle Méthode (Délai Calculé)

La nouvelle méthode utilise la longueur du message (message | length) pour estimer de manière dynamique le temps de lecture, ajoutant une marge de sécurité.
A. Le Calcul Dynamique
calculated_delay: "{{ ((message | length / 12) + 3) | round(0) }}"
message | length / 12: On divise la longueur totale du message (nombre de caractères) par une estimation du taux de parole (environ 12 caractères par seconde). Cela donne une bonne approximation de la durée de lecture.
+ 3: On ajoute 3 secondes de marge de sécurité. Ces secondes sont essentielles pour couvrir la latence du réseau, le temps de chargement du fichier TTS sur l'appareil Cast, et le temps de transition.
| round(0): On arrondit le résultat à la seconde près.
B. La Mise en Œuvre dans l’Action
- variables:
calculated_delay: ...
- delay:
seconds: "{{ calculated_delay | int }}"
En remplaçant le wait_template fixe par un delay basé sur ce calcul :
Fiabilité Accrue : L’automatisation attendra juste assez longtemps pour que même les messages longs (comme la liste des ouvrants ouverts) aient le temps d’être complètement diffusés.
Performance Optimale : Dès que le temps calculé est écoulé, le volume est baissé sans attendre un état hypothétique, et le script ne reste pas bloqué inutilement.
En résumé, on passe d’une attente basée sur un état incertain et un temps fixe à une attente basée sur une estimation de la durée réelle du contenu.
le code complet du diffuseur contextuel V2
alias: TTS - Diffuseur contextuel
description: Diffuse un message dans les pièces où une présence est détectée
triggers:
- event_type: dummy_startup_never_fired
trigger: event
conditions:
- condition: template
value_template: "{{ occupied_rooms | length > 0 }}"
- condition: template
value_template: "{{ door_message is defined and door_message | length > 0 }}"
actions:
- target:
entity_id: "{{ occupied_rooms }}"
data:
volume_level: 0.8
action: media_player.volume_set
- delay: "00:00:01"
- target:
entity_id: tts.google_en_com
data:
cache: true
media_player_entity_id: "{{ occupied_rooms }}"
message: "{{ message }}"
language: fr
action: tts.speak
- variables:
calculated_delay: "{{ ((message | length / 12) + 3) | round(0) }}"
- delay:
hours: 0
minutes: 0
seconds: "{{ calculated_delay | int }}"
milliseconds: 0
- target:
entity_id: "{{ occupied_rooms }}"
data:
volume_level: 0.5
action: media_player.volume_set
mode: queued
variables:
message: "{{ trigger.variables.message | default('') }}"
occupied_rooms: >
{% set rooms = [] %} {% if states('sensor.presence_entree') == 'Entree' %}{%
set rooms = rooms + ['media_player.entree'] %}{% endif %} {% if
states('sensor.presence_cagibi') == 'Cagibi' %}{% set rooms = rooms +
['media_player.entree'] %}{% endif %} {% if
states('sensor.presence_cuisine') == 'Cuisine' %}{% set rooms = rooms +
['media_player.cuisine'] %}{% endif %} {% if
states('sensor.presence_salle_manger') == 'Salle_manger' %}{% set rooms =
rooms + ['media_player.salon'] %}{% endif %} {% if
states('sensor.presence_chaufferie') == 'Chaufferie' %}{% set rooms = rooms
+ ['media_player.chaufferie'] %}{% endif %} {% if
states('sensor.presence_labo') == 'Labo' %}{% set rooms = rooms +
['media_player.labo'] %}{% endif %} {% if
states('sensor.presence_garage_interne') == 'Garage_interne' %}{% set rooms
= rooms + ['media_player.garage_interne'] %}{% endif %} {% if
states('sensor.presence_veranda') == 'Veranda' %}{% set rooms = rooms +
['media_player.veranda'] %}{% endif %} {% if states('sensor.presence_wc') ==
'Wc' %}{% set rooms = rooms + ['media_player.toilettes'] %}{% endif %} {% if
states('sensor.presence_chambre_laurent') == 'Chambre_laurent' %}{% set
rooms = rooms + ['media_player.chambre_laurent'] %}{% endif %} {% if
states('sensor.presence_chambre_papa') == 'Chambre_papa' %}{% set rooms =
rooms + ['media_player.chambre_papa'] %}{% endif %} {% if
states('sensor.presence_chambre_amis') == 'Chambre_amis' %}{% set rooms =
rooms + ['media_player.chambre_amis'] %}{% endif %} {% if
states('sensor.presence_sdb') == 'Sdb' %}{% set rooms = rooms +
['media_player.salle_de_bain'] %}{% endif %} {% if
states('sensor.presence_garage_externe') == 'Garage_externe' %}{% set rooms
= rooms + ['media_player.garage_externe'] %}{% endif %} {{ rooms | unique |
list }}
On peut facilement envisager la surveillance de n’importe quoi sur un état que l’on ne sohaite pas. Ici ce sont les portes, j’ai réglé sur 5 minutes les répétitions, je vais également faire la même chose sur les portes congélateurs et frigo avec un temps de répétition plus court. Mais tout est envisageable.
Logs
Effectuer l'action « Automatisation: Déclencher » sur TTS - Diffuseur contextuel
Itération 1
Exécuté : 22 novembre 2025 à 10:19:25
Résultat :
params:
domain: automation
service: trigger
service_data:
skip_condition: true
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : Porte2 garage
exterieur, Porte1 garage exterieur
entity_id:
- automation.tts_diffuseur_contextuel
target:
entity_id:
- automation.tts_diffuseur_contextuel
running_script: true
child_id:
domain: automation
item_id: '1751786922063'
run_id: d650472aede808d36221aab53892647b
Effectuer l'action « Automatisation: Déclencher » sur TTS - Diffuseur contextuel
Itération 2
Exécuté : 22 novembre 2025 à 10:20:39
Résultat :
params:
domain: automation
service: trigger
service_data:
skip_condition: true
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : Porte3 garage
exterieur, Porte2 garage exterieur, Porte1 garage exterieur
entity_id:
- automation.tts_diffuseur_contextuel
target:
entity_id:
- automation.tts_diffuseur_contextuel
running_script: true
child_id:
domain: automation
item_id: '1751786922063'
run_id: b1367481b46acafe431f7dd9450a46e9
Effectuer l'action « Automatisation: Déclencher » sur TTS - Diffuseur contextuel
Itération 3
Exécuté : 22 novembre 2025 à 10:21:54
Résultat :
params:
domain: automation
service: trigger
service_data:
skip_condition: true
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : Porte3 garage
exterieur, Porte2 garage exterieur, Porte1 garage exterieur
entity_id:
- automation.tts_diffuseur_contextuel
target:
entity_id:
- automation.tts_diffuseur_contextuel
running_script: true
child_id:
domain: automation
item_id: '1751786922063'
run_id: c6438103faad68e3e2ce1b5a2ee56c54
Effectuer l'action « Automatisation: Déclencher » sur TTS - Diffuseur contextuel
Itération 4
Exécuté : 22 novembre 2025 à 10:23:09
Résultat :
params:
domain: automation
service: trigger
service_data:
skip_condition: true
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : Porte3 garage
exterieur, Porte2 garage exterieur, Porte1 garage exterieur
entity_id:
- automation.tts_diffuseur_contextuel
target:
entity_id:
- automation.tts_diffuseur_contextuel
running_script: true
child_id:
domain: automation
item_id: '1751786922063'
run_id: cb81fec9892cf7b0c7013f9888b6efc9
Effectuer l'action « Automatisation: Déclencher » sur TTS - Diffuseur contextuel
Itération 5
Exécuté : 22 novembre 2025 à 10:24:24
Résultat :
params:
domain: automation
service: trigger
service_data:
skip_condition: true
variables:
message: >-
Attention, les ouvrants suivants sont toujours ouverts : Porte1 garage
exterieur
entity_id:
- automation.tts_diffuseur_contextuel
target:
entity_id:
- automation.tts_diffuseur_contextuel
running_script: true
child_id:
domain: automation
item_id: '1751786922063'
run_id: ded2d22c638a6e3d1a63d8909614bac6
j’avais passé le timer à 1minute pour les tests
cdt