Sommaire
Introduction
Qui n'a jamais rêvé d'un système de notifications intelligentes qui s'adapte automatiquement à votre position dans la maison. Grâce à des capteurs de présence, l'IA générative et des assistants vocaux, vos notifications vous suivront dans chaque pièce.
Nous aborderons deux parties distinctes et deux implémentations différentes, la première avec Alexa, et la deuxième avec Google home.
Ils couvriront un large spectre d'utilisations et seront modulaires à souhait en passant par les scripts ou les automations ou les deux. À vous de choisir ce qui vous convient le mieux.
Prérequis
- Home Assistant installé et configuré.
- Capteurs de présence LD2410C configuré sur un esp32 dans esphome (mais on peut imaginer de la triangulation bluetooth, ou autres).
- Amazon Echo ou Google Home/Nest ou tout media_player dans chaque pièce (ou pas, nous y reviendrons).
- Intégration Google Generative AI (Gemini) (c'est complètement facultatif, mais tellement plus fun).
- Connaissances de base en YAML.
La version Alexa + scripts by @Gael
Dans les grandes lignes, ma domotique va identifier la pièce occupée grâce aux LD2410 disséminés aux 4 coins de mon château de 40m2, et associer une enceinte Echo à cette pièce.
Elle va ensuite générer une phrase par IA, suivant des directives précises (prompt). Cela évite d'avoir toujours la même phrase pour un même événement.
Dernière étape, la "moulinette" du script : après avoir récupéré le volume de l'enceinte, on monte le son, histoire d’être assuré d'entendre ce qu'elle a à nous dire, on lance la phrase générée par l'IA sur la bonne enceinte, celle de la pièce occupée, et enfin on remet le volume tel qu'il était.
Configuration des capteurs LD2410C
Les capteurs LD2410C sont configurés via ESPHome pour détecter la présence dans chaque pièce.
La configuration détaillée est présentée dans ce précédent article (@freetronic) :

N'hésitez pas à également vous référer à la documentation ESPHome décrivant l'intégration du LD2410C.
Sensor Template de localisation
Ici, nous allons détecter quelle pièce est occupée, et quelle enceinte lui associer. J'ai commenté le script pour plus de clarté.
Configuration du sensor :
template:
- sensor:
# Création d'un capteur nommé "Presence Piece"
- name: "Presence Piece"
# Définition de l'état du capteur selon les capteurs de présence activés
state: >
{% if states('binary_sensor.esp_salon_presence_2') == 'on' %}
Salon # Si une présence est détectée dans le salon
{% elif states('binary_sensor.esp_cuisine_presence') == 'on' %}
Cuisine # Si une présence est détectée dans la cuisine (et pas dans le salon)
{% elif states('binary_sensor.esp_chambre_presence') == 'on' %}
Chambre # Si une présence est détectée dans la chambre (et pas dans les pièces précédentes)
{% elif states('binary_sensor.esp_sdb_presence') == 'on' %}
SdB # Si une présence est détectée dans la salle de bain (et pas dans les pièces précédentes)
{% else %}
'' # Si aucune présence n’est détectée dans aucune des pièces listées
{% endif %}
attributes:
# Ajout d'un attribut personnalisé nommé "echo"
# Cet attribut détermine quel appareil Echo est associé à la pièce détectée
echo: >
{% if states('sensor.presence_piece') == 'Salon' %}
media_player.echo_studio_d # Echo du salon
{% elif states('sensor.presence_piece') == 'Cuisine' %}
media_player.echo_show_cuisine # Echo de la cuisine
{% elif states('sensor.presence_piece') == 'Chambre' %}
media_player.echo_show_chambre # Echo de la chambre
{% elif states('sensor.presence_piece') == 'SdB' %}
media_player.echo_sdb # Echo de la salle de bain
{% else %}
media_player.echo_dot_gael # Echo par défaut si aucune pièce n’est détectée
{% endif %}
Ceci un exemple de base, on peut y ajouter des attributs, comme le nombre de pièces ou leur nom, mais aussi des exceptions ou exclusions.
Remarques :
- L’ordre des
elif
est important : la première pièce détectant une présence l’emporte. - Le capteur
sensor.presence_piece
peut être utilisé ailleurs pour centraliser les automatismes ou annonces vocales en fonction de la pièce occupée. - L’attribut
echo
permet d’associer dynamiquement un appareil Echo à la pièce où une présence a été détectée, utile pour les TTS ou commandes vocales localisées.
Voici un exemple beaucoup plus poussé :
Le capteur surveille 4 pièces via leurs capteurs de présence :
- Salon : 2 capteurs (esp_salon_presence + esp_multi_capteur_presence)
- Cuisine : 1 capteur (esp_cuisine_presence)
- Chambre : 1 capteur (esp_chambre_presence)
- Salle de bain : 1 capteur (esp_sdb_presence)
Logique de sélection :
Si une seule pièce est occupée → Sélectionne cette pièce directement
Si plusieurs pièces sont occupées → Applique un ordre de priorité :
- Salon
- Cuisine
- Chambre
- Salle de bain
Gestion spéciale de la salle de bain :
La salle de bain est automatiquement exclue si :
- Sa fenêtre est ouverte OU
- Le switch "prismal" est allumé
(Ceci évite les fausses détections dans cette pièce)
Résultat :
Le capteur retourne :
- Le nom de la pièce prioritaire
- L'Alexa correspondante (attribut
echo
) - Des informations de débogage (toutes les pièces occupées, nombre, etc.)
## Capteur pour déterminer quelle Alexa utiliser selon la pièce occupée ##
- sensor:
- name: "Presence Piece"
unique_id: presence_piece
state: >
# Mappage des pièces avec leurs capteurs de présence respectifs
# Chaque pièce peut avoir plusieurs capteurs pour une détection plus fiable
{% set presence_map = {
'Salon': ['binary_sensor.esp_salon_presence', 'binary_sensor.esp_multi_capteur_presence'],
'Cuisine': ['binary_sensor.esp_cuisine_presence'],
'Chambre': ['binary_sensor.esp_chambre_presence'],
'SdB': ['binary_sensor.esp_sdb_presence']
} %}
# Utilisation d'un namespace pour stocker la liste des pièces occupées
# (nécessaire car les variables Jinja2 sont immutables par défaut)
{% set ns = namespace(pieces_occupees=[]) %}
# Parcours de chaque pièce et de ses capteurs
{% for piece, capteurs in presence_map.items() %}
{% for capteur in capteurs %}
# Si au moins un capteur de la pièce détecte une présence
{% if is_state(capteur, 'on') %}
# Ajouter la pièce à la liste des pièces occupées
{% set ns.pieces_occupees = ns.pieces_occupees + [piece] %}
# Sortir de la boucle des capteurs pour cette pièce (un seul suffit)
{% break %}
{% endif %}
{% endfor %}
{% endfor %}
# Logique de priorité pour déterminer quelle pièce retourner
{% if ns.pieces_occupees | length == 0 %}
# Aucune présence détectée - ne rien retourner
{% elif ns.pieces_occupees | length == 1 %}
# Une seule pièce détectée - retourner directement cette pièce
{{ ns.pieces_occupees[0] }}
{% elif ns.pieces_occupees | length > 1 %}
# Plusieurs pièces détectées - appliquer la logique de priorité
# Gestion spéciale pour la salle de bain (SdB)
{% if 'SdB' in ns.pieces_occupees and (is_state('binary_sensor.ouvfenetsdb_contact', 'on') or is_state('switch.prismal', 'on')) %}
# Si SdB détectée MAIS fenêtre ouverte OU switch prismal allumé
# alors exclure la SdB des candidats (probablement fausse détection)
{% set pieces_candidates = ns.pieces_occupees | reject('eq', 'SdB') | list %}
{% else %}
# Sinon, garder toutes les pièces comme candidates
{% set pieces_candidates = ns.pieces_occupees %}
{% endif %}
# Appliquer l'ordre de priorité sur les candidats restants
# Ordre de priorité : Salon > Cuisine > Chambre > SdB
{% if 'Salon' in pieces_candidates %}
Salon
{% elif 'Cuisine' in pieces_candidates %}
Cuisine
{% elif 'Chambre' in pieces_candidates %}
Chambre
{% elif 'SdB' in pieces_candidates %}
SdB
{% else %}
# Cas de secours si toutes les pièces ont été exclues
# (ne devrait pas arriver normalement)
{{ ns.pieces_occupees[0] }}
{% endif %}
{% endif %}
attributes:
# Attribut pour déterminer quelle Alexa utiliser selon la pièce active
echo: >
# Mappage des pièces vers leurs dispositifs Alexa correspondants
{% set echo_map = {
'Salon': 'media_player.echo_studio_d',
'Cuisine': 'media_player.echo_show_cuisine',
'Chambre': 'media_player.echo_show_chambre',
'SdB': 'media_player.echo_sdb'
} %}
# Retourner l'Alexa de la pièce active, ou l'Alexa par défaut si aucune pièce
{{ echo_map.get(states('sensor.presence_piece'), 'media_player.echo_dot_gael') }}
# Attribut pour lister toutes les pièces actuellement occupées
pieces_occupees: >
# Même logique que dans le state principal mais pour affichage
{% set presence_map = {
'Salon': ['binary_sensor.esp_salon_presence', 'binary_sensor.esp_multi_capteur_presence'],
'Cuisine': ['binary_sensor.esp_cuisine_presence'],
'Chambre': ['binary_sensor.esp_chambre_presence'],
'SdB': ['binary_sensor.esp_sdb_presence']
} %}
{% set ns = namespace(pieces_occupees=[]) %}
{% for piece, capteurs in presence_map.items() %}
{% for capteur in capteurs %}
{% if is_state(capteur, 'on') %}
{% set ns.pieces_occupees = ns.pieces_occupees + [piece] %}
{% break %}
{% endif %}
{% endfor %}
{% endfor %}
# Joindre toutes les pièces occupées avec une virgule
{{ ns.pieces_occupees | join(', ') }}
# Attribut pour compter le nombre de pièces occupées
nombre_pieces_occupees: >
{% set presence_map = {
'Salon': ['binary_sensor.esp_salon_presence', 'binary_sensor.esp_multi_capteur_presence'],
'Cuisine': ['binary_sensor.esp_cuisine_presence'],
'Chambre': ['binary_sensor.esp_chambre_presence'],
'SdB': ['binary_sensor.esp_sdb_presence']
} %}
{% set ns = namespace(pieces_occupees=[]) %}
{% for piece, capteurs in presence_map.items() %}
{% for capteur in capteurs %}
{% if is_state(capteur, 'on') %}
{% set ns.pieces_occupees = ns.pieces_occupees + [piece] %}
{% break %}
{% endif %}
{% endfor %}
{% endfor %}
# Retourner le nombre de pièces occupées
{{ ns.pieces_occupees | length }}
# Attribut pour savoir si la SdB a été exclue de la sélection
sdb_exclue: >
{% set presence_map = {
'Salon': ['binary_sensor.esp_salon_presence', 'binary_sensor.esp_multi_capteur_presence'],
'Cuisine': ['binary_sensor.esp_cuisine_presence'],
'Chambre': ['binary_sensor.esp_chambre_presence'],
'SdB': ['binary_sensor.esp_sdb_presence']
} %}
{% set ns = namespace(pieces_occupees=[]) %}
{% for piece, capteurs in presence_map.items() %}
{% for capteur in capteurs %}
{% if is_state(capteur, 'on') %}
{% set ns.pieces_occupees = ns.pieces_occupees + [piece] %}
{% break %}
{% endif %}
{% endfor %}
{% endfor %}
# Retourner True si SdB est occupée ET (fenêtre ouverte OU switch prismal allumé)
{{ 'SdB' in ns.pieces_occupees and (is_state('binary_sensor.ouvfenetsdb_contact', 'on') or is_state('switch.prismal', 'on')) }}
# Attribut pour savoir si la fenêtre de la SdB est ouverte
fenetre_sdb_ouverte: >
{{ is_state('binary_sensor.ouvfenetsdb_contact', 'on') }}
# Attribut pour savoir si le switch prismal est allumé
switch_prismal_on: >
{{ is_state('switch.prismal', 'on') }}
Intégration Google Generative AI
Ajouter l'intégration Google Generative AI et suivre les instructions.
L'IA va nous servir à générer des phrases, et à mon avis, ajouter un peu de fun. On peut bien sûr utiliser des phrases figées du genre "le café est prêt", ou une liste qu'on ira chercher aléatoirement, mais l'IA générative apporte vraiment un plus.
Voici un exemple de prompt :
action: google_generative_ai_conversation.generate_content
data:
prompt: |-
Génère un message vocal pour prévenir que le café est prêt.
Le ton doit être court, factuel, avec une touche d'humour ou de sarcasme
léger, dans le style d'un droïde reprogrammé façon K-2SO (Star Wars).
Tu peux glisser une référence geek ou pop culture si c'est pertinent.
La réponse doit être adaptée au TTS : courte, claire, sans smileys ni
émoticônes.
Aucune insulte, aucune menace.
Exemples de ton attendu :
« Le café est prêt. Vous avez survécu jusque-là, autant continuer. »
« Café disponible. Taux de réveil cérébral à suivre… »
« Mission accomplie : café prêt. J'espère que c'est assez fort. »
« Activation de la routine café terminée. Bonne chance pour la suite. »
Génère uniquement la phrase, sans explications, sans balises, sans
métadonnées.
Anatomie du prompt Gemini
Structure du prompt :
- Contexte : "Génère un message vocal pour prévenir que le café est prêt".
- Personnalité : Style K-2SO avec humour/sarcasme léger.
- Contraintes techniques : Adapté au TTS, sans émoticônes.
- Exemples : 4 exemples concrets du ton attendu.
- Instruction finale : "Génère uniquement la phrase".
Bonnes pratiques pour les prompts :
- Soyez précis sur le contexte.
- Définissez la personnalité souhaitée.
- Donnez des exemples concrets.
- Spécifiez les contraintes (longueur, format).
- Limitez la réponse à l'essentiel.
Scripts de notification
Le cœur du système repose sur un script intelligent qui gère l'envoi des notifications avec contrôle du volume et ciblage automatique.
Je trouve la gestion avec un script plus souple. Plutôt que de remettre l'ensemble des actions dans les différents scénarios qui peuvent envoyer une notification, le script est un "résumé" de ces actions dans lequel on n'aura qu'à ajouter les variables inhérentes du scénario qui a appelé le script.
Script principal : notification_alexa
alias: "Notification Alexa"
sequence:
- choose:
- conditions:
# Vérifie la présence à domicile
- condition: state
entity_id: person.canabang
state: home
# Vérifie que c'est la journée (évite les notifications nocturnes)
- condition: state
entity_id: text.cycle
state: jour
sequence:
- variables:
# Récupère l'Echo de la pièce occupée
echo: "{{ state_attr('sensor.presence_piece', 'echo') }}"
# Sauvegarde le volume actuel
volume_precedent: "{{ state_attr( echo , "volume_level") }}"
# Configure le volume pour la notification
- action: media_player.volume_set
target:
entity_id: |
{{ echo }}
data:
volume_level: 0.4
enabled: true
# Envoie la notification TTS
- action: notify.alexa_media
data:
message: "{{ message }}"
data:
type: tts
target: |
{{ state_attr('sensor.presence_piece', 'echo') }}
# Délai pour laisser le temps au TTS
- delay:
hours: 0
minutes: 0
seconds: 8
milliseconds: 0
# Restaure le volume précédent
- action: media_player.volume_set
target:
entity_id: |
{{ echo }}
data:
volume_level: |
{{ volume_precedent }}
enabled: true
mode: single
fields:
message:
selector:
text: {}
name: message
description: "Message à diffuser"
required: true
Comment créer un script :
Donner un nom et ajouter des champs.
On peut créer autant de champs que nécessaire, ils servent à définir les "variables"
Pour la notification, ici un seul champ suffit, il s’appelle message.
Editer en yaml et modifier la valeur text : null par text : {}
message:
selector:
text: {}
name: message
description: message pour la notification
required: true
Et on ajoute l'action "appeler un service".
...que l'on modifie en yaml avec ce code :
service: notify.alexa_media
data:
message: "{{ message }}"
data:
type: tts
target: |
{{ state_attr('sensor.presence_piece', 'echo') }}
{{ message }} pour aller chercher l'info dans le champ
{{ state_attr('sensor.presence_piece', 'echo') }} , va chercher l’attribut "echo" du sensor template créé précédemment
Fonctionnalités du script
Conditions intelligentes :
- Vérifie la présence à domicile (évite les notifications en absence)
- Contrôle du cycle jour/nuit (respecte les heures de sommeil)
Gestion du volume :
- Sauvegarde du volume actuel
- Volume standardisé pour les notifications (0.4)
- Restauration automatique du volume précédent
Ciblage automatique :
- Utilise l'attribut
echo
du sensor de présence. - Envoi sur l'Echo de la pièce occupée.
- Fallback sur l'Echo principal si aucune pièce détectée.
Gestion des cas particuliers
Si aucune pièce occupée : Le sensor template renvoie une chaîne vide, et l'attribut echo
utilise le media_player.echo_dot_gael
comme fallback.
Si plusieurs Echos disponibles : Le script cible uniquement l'Echo de la pièce prioritaire selon la logique du sensor template.
Délai d'attente : Les 8 secondes permettent au TTS de se terminer avant la restauration du volume. Ce délai peut être ajusté selon la longueur moyenne de vos messages.
Exemple complet : Notification café prêt
Voici un exemple concret d'automatisation qui combine génération IA et notification dynamique :
Automation ou script appelé quand le café est prêt
sequence:
# Génération du message avec Gemini
- action: google_generative_ai_conversation.generate_content
data:
prompt: >+
Génère un message vocal pour prévenir que le café est prêt.
Le ton doit être court, factuel, avec une touche d'humour ou de sarcasme
léger, dans le style d'un droïde reprogrammé façon K-2SO (Star Wars).
Tu peux glisser une référence geek ou pop culture si c'est pertinent.
La réponse doit être adaptée au TTS : courte, claire, sans smileys ni
émoticônes.
Aucune insulte, aucune menace.
Exemples de ton attendu :
« Le café est prêt. Vous avez survécu jusque-là, autant continuer. »
« Café disponible. Taux de réveil cérébral à suivre… »
« Mission accomplie : café prêt. J'espère que c'est assez fort. »
« Activation de la routine café terminée. Bonne chance pour la suite. »
Génère uniquement la phrase, sans explications, sans balises, sans
métadonnées.
response_variable: generated_message
# Envoi de la notification
- action: script.notification_alexa
data:
message: "{{ generated_message.text }}"
enabled: true
On stocke la phrase de l'IA dans la variable "generated_message" et on la rappel en ajoutant .text "generated_message.text". Si on ne l'ajoute pas, la lecture du TTS se fera sur la phrase complète, incluant les caractères spéciaux.
text: >
Le café est prêt. Optimisation des performances cognitives initiée. Que la
Force soit avec vous... vous en aurez besoin.
Ici "text : >" sera donc compris dans le TTS
quelques exemples de phrases générées :
- Café prêt. N'espérez pas un miracle, c'est juste du café.
- Café prêt. Il est temps d'affronter la réalité.
- Café prêt. Ne me remerciez pas, c'est dans ma programmation.
- Le café est prêt. Ne vous emballez pas, la journée ne fait que commencer.
Si, vous n'utilisez pas d'IA generative, il suffit juste de faire une action et de remplacer {{ generated_message.text }} par le message voulu.
# Automation ou script appelé quand le café est prêt
action: script.notification_alexa
data:
message: "le café est prêt"
enabled: true
Bilan de cette première implémentation
Ce système offre une expérience utilisateur fluide et personnalisée. Les notifications vous suivent naturellement dans votre quotidien, s'adaptant à votre position et au contexte.
La combinaison capteurs de présence + IA générative + assistants vocaux ouvre de nombreuses possibilités créatives pour améliorer votre maison connectée.
La version Google home + automations, multiroom by @Freetronic
Gestion du multi room / messages / personnes
Vue d'ensemble
Autre approche sur la même base que la version Alexa (j'ai volontairement supprimé une partie des triggers et sensors dans un souci de compréhension et de simplification du code, la partie variable est complète par contre).
Le système fonctionne en 4 étapes :
- Détection : Les capteurs LD2410C détectent votre présence. (ça fonctionne avec des capteurs de mouvement, mais forcément le temps de présence dans la pièce est moins précis).
- Localisation : Un sensor template détermine la pièce occupée.
- Génération : un message est généré
- Diffusion : Le message est envoyé sur le Google Home/ Nest de la (ou des) pièce(s) concernée(s).
Composants nécessaires
- Capteurs LD2410C (un ou plusieurs par pièce).
- ESP32 pour chaque capteur.
- Google Home / Nest par pièce (facultatif).
- Intégration Google Cast.
Outils de tests facultatifs
Voici deux cartes pour vérifier où le son est joué et une carte de vérification de la présence :

type: entities
entities:
- entity: sensor.presence_labo
- entity: sensor.presence_entree
- entity: sensor.presence_cuisine

type: history-graph
entities:
- entity: media_player.labo
- entity: media_player.entree
- entity: media_player.cuisine
hours_to_show: 0.15
Sensor Template de localisation
Configuration du sensor
À la différence de la première partie où un template sensor global est utilisé, ici on utilisera un template sensor par pièce.
Ici 2 cas de figures, on reprend juste les noms des entités des media_player et des détecteurs de présence ou de mouvement pour gérer la présence dans une pièce et la diffusion sur tel ou tel media player.


La version un Google Home / Nest pour plusieurs pièces :
template:
- sensor:
- name: "Presence Entree"
state: >
{% if states('binary_sensor.esp1_entree_radar_target') == 'on' or states('binary_sensor.esp2_entree_radar_target') == 'on' %}
Entree
{% else %}
''
{% endif %}
attributes:
echo: "media_player.toilettes, media_player.cuisine"
- name: "Presence Cuisine"
state: >
{% if states('binary_sensor.esp1_cuisine_radar_target') == 'on' or states('binary_sensor.esp2_cuisine_radar_target') == 'on' %}
Cuisine
{% else %}
''
{% endif %}
attributes:
echo: "media_player.cuisine"
- name: "Presence Labo"
state: >
{% if states('binary_sensor.esp1_labo_radar_target') == 'on' %}
Labo
{% else %}
''
{% endif %}
attributes:
echo: "media_player.labo"
Voici la version un Google Home / Nest pour une pièce (à partir d'ici, je pars du postulat que c'est un Google Home / Nest par pièce).
template:
- sensor:
- name: "Presence Entree"
state: >
{% if states('binary_sensor.esp1_entree_radar_target') == 'on' or states('binary_sensor.esp2_entree_radar_target') == 'on' %}
Entree
{% else %}
''
{% endif %}
attributes:
echo: "media_player.entree"
- name: "Presence Cuisine"
state: >
{% if states('binary_sensor.esp1_cuisine_radar_target') == 'on' or states('binary_sensor.esp2_cuisine_radar_target') == 'on' %}
Cuisine
{% else %}
''
{% endif %}
attributes:
echo: "media_player.cuisine"
- name: "Presence Labo"
state: >
{% if states('binary_sensor.esp1_labo_radar_target') == 'on' %}
Labo
{% else %}
''
{% endif %}
attributes:
echo: "media_player.labo"
Structure globale
Utilisation d'une section template:
avec un type sensor:
pour créer des capteurs virtuels (des entités sensor.
) dont la valeur (state
) est déterminée dynamiquement via un template Jinja2.
Chaque capteur a :
- un nom : (
name
) qui devient le nom visible du capteur - un état : (
state
) calculé par un blocif
- des attributs personnalisés : (dans ce cas,
echo
)
- name: "Presence Entree"
state: >
{% if states('binary_sensor.esp1_entree_radar_target') == 'on' or states('binary_sensor.esp2_entree_radar_target') == 'on' %}
Entree
{% else %}
''
{% endif %}
attributes:
echo: "media_player.entree"
- But : détecter une présence dans l’entrée.
- Logique : si au moins un des deux capteurs radars (
esp1
ouesp2
) détecte une présence (== 'on'
), alors l'état du capteur devient"Entree"
, sinon il reste vide (''
). - Attribut
echo
: liste desmedia_player
liés aux assistants vocaux dans cette pièce (Echo Entree).
Pourquoi cette structure est utile
- Séparation claire par zone : chaque pièce a son propre capteur de présence logique.
- Simplicité d’usage : réutilisation ces capteurs dans des automatisations, des dashboards ou des annonces vocales.
- Flexibilité : en centralisant les assistants dans un attribut
echo
, On peut facilement cibler des zones spécifiques pour envoyer des messages, alarmes, ou alertes.
Je vais ici détailler tout le cheminement pour trouver la façon de faire, et les tests effectués, certaines choses plus simples pouvant tout à fait convenir à certains.
Couplé à une automation pour vérifier le changement de pièce, et notifier la présence dans une pièce en temps réel. On fixe le volume, puis après la notification. On repasse sur un volume plus faible, les wait_template peuvent être ajustés dans une certaine mesure en fonction de la taille des messages.
Objectif général de l'automatisation
Cette automatisation déclenche une notification vocale via un assistant vocal lorsqu'une présence est détectée dans l’entrée, la cuisine ou le labo. Elle utilise les capteurs personnalisés sensor.presence_*
définis précédemment, et ajuste automatiquement le volume sonore pour la clarté du message.
On fixe le volume, puis après la notification. On repasse sur un volume plus faible, les wait_template peuvent être ajustés dans une certaine mesure en fonction de la taille des messages.
`alias: notification gh suivant presence
description: ""
triggers:
- entity_id:
- sensor.presence_entree
- sensor.presence_cuisine
- sensor.presence_labo
to:
- Entree
- Cuisine
- Labo
trigger: state
actions:
- variables:
media_players: "{{ state_attr(trigger.entity_id, 'echo') }}"
- choose:
- conditions:
- condition: template
value_template: "{{ media_players is not none }}"
sequence:
- action: media_player.volume_set
target:
entity_id: "{{ media_players }}"
data:
volume_level: 0.8
- target:
entity_id: tts.google_en_com
data:
cache: true
media_player_entity_id: "{{ media_players }}"
message: détection{{ trigger.to_state.state }}
language: fr
action: tts.speak
- wait_template: "{{ is_state('media_players', 'playing') }}"
timeout: "00:00:01"
- wait_template: "{{ is_state('media_players', 'idle') }}"
timeout: "00:00:05"
- action: media_player.volume_set
target:
entity_id: "{{ media_players }}"
data:
volume_level: 0.5
mode: queued`
Déclencheur (trigger
)
triggers:
- entity_id:
- sensor.presence_entree
- sensor.presence_cuisine
- sensor.presence_labo
to:
- Entree
- Cuisine
- Labo
trigger: state
- L'automatisation se déclenche quand l'un des trois capteurs de présence change d’état vers un état non vide (donc "Entree", "Cuisine" ou "Labo").
- Le déclencheur est basé sur un changement d’état.
- variables:
media_players: "{{ state_attr(trigger.entity_id, 'echo') }}"
- On crée une variable
media_players
qui contient la liste des assistants vocaux (media_player) associés à la zone où la présence a été détectée. - Cela récupère dynamiquement l’attribut
echo
depuis l’entité qui a déclenché l'automatisation (trigger.entity_id
).
- choose:
- conditions:
- condition: template
value_template: "{{ media_players is not none }}"
On vérifie que la variable media_players
contient bien une ou plusieurs entités (évite d’exécuter si l'attribut est vide ou mal défini).
sequence:
- action: media_player.volume_set
target:
entity_id: "{{ media_players }}"
data:
volume_level: 0.8
Monte le volume à 80% pour être sûr que le message soit bien entendu.
- target:
entity_id: tts.google_en_com
data:
cache: true
media_player_entity_id: "{{ media_players }}"
message: détection{{ trigger.to_state.state }}
language: fr
action: tts.speak
Utilise le service tts.speak
pour faire parler l’assistant vocal, avec un message dynamique comme :
« détection Entree » ou « détection Cuisine »
- Le message est prononcé en français (
language: fr
). - Le TTS (text-to-speech) est envoyé aux assistants vocaux récupérés via
media_players
.
- wait_template: "{{ is_state('media_players', 'playing') }}"
timeout: "00:00:01"
Attend que la lecture commence (ou passe si ça ne démarre pas dans la seconde).
- wait_template: "{{ is_state('media_players', 'idle') }}"
timeout: "00:00:05"
Puis attend que la lecture soit terminée (jusqu’à 5 secondes maximum).
- action: media_player.volume_set
target:
entity_id: "{{ media_players }}"
data:
volume_level: 0.5
Remet le volume à 50% pour ne pas déranger les prochaines utilisations de Google home/nest hors de ce contexte.
Nous pouvons de fait tester en pénétrant dans une pièce que la notification fonctionne bien : montée du volume, message "détection" joué, retour au volume fixé.
Vient ensuite la variable qu'on va utiliser pour savoir dans quelle pièce on doit jouer le TTS qu'on va également mettre dans une automation complète pour vérifier le fonctionnement, et qui peut être utilisée en l'état. Les entités sont connues, elles sont dans le template du début.
variables:
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 }}
Ce bloc de code déclare une variable appelée occupied_rooms
, qui contient une liste des media_player
correspondant aux pièces actuellement occupées. Elle s’utilise ensuite dans des automatisations ou scripts, comme les annonces vocales multi-pièces.
variables:
occupied_rooms: >
- Définition d’une variable de contexte appelée
occupied_rooms
. - Le contenu est un template Jinja qui retourne une liste d’enceintes vocales (
media_player.*
) des pièces occupées.
{% set rooms = [] %}
Création d’une liste vide rooms
pour y ajouter les pièces occupées.
{% if states('sensor.presence_entree') == 'Entree' %}
{% set rooms = rooms + ['media_player.entree'] %}
{% endif %}
- Si le capteur
sensor.presence_entree
a la valeur"Entree"
(présence détectée), on ajoutemedia_player.entree
à la listerooms
.
Ce schéma est répété pour chaque pièce :
sensor.presence_cuisine
→media_player.cuisine
sensor.presence_labo
→media_player.labo
- etc.
Particularité : mapping personnalisé
Certains capteurs sont liés à une enceinte dans une autre pièce :
{% if states('sensor.presence_cagibi') == 'Cagibi' %}
{% set rooms = rooms + ['media_player.entree'] %}
{% endif %}
Exemple : si le cagibi est occupé, on utilise l’enceinte de l’entrée (media_player.entree
), car le cagibi n’a pas sa propre enceinte.
Nettoyage de la liste
{{ rooms | unique | list }}
- Une fois toutes les pièces vérifiées :
| unique
: retire les doublons (au cas où une même enceinte est utilisée pour plusieurs pièces).| list
: convertit le résultat en liste Python classique.
Par exemple, si media_player.entree
est ajouté deux fois (entrée + cagibi), il ne sera conservé qu'une seule fois.
On pourra tester directement dans les outils dev.

Et l'automation où l'on va tester que tout fonctionne en se déplaçant dans les pièces et en surveillant les ouvrants (on ne sera notifié que dans la pièce, ou les pièces où il y a présence) :
alias: TTS Porte ouverte - présence ciblée1
description: Annonce les ouvertures de porte dans la pièce où il y a de la présence
triggers:
- entity_id:
- binary_sensor.porte_entree_2
- binary_sensor.porte_chaufferie_labopi2
- binary_sensor.porte_labo_labopi2
from: "off"
to: "on"
trigger: state
conditions:
- condition: template
value_template: "{{ occupied_rooms | length > 0 }}"
actions:
- target:
entity_id: "{{ occupied_rooms }}"
data:
volume_level: 0.8
action: media_player.volume_set
- target:
entity_id: tts.google_en_com
data:
cache: true
media_player_entity_id: "{{ occupied_rooms }}"
message: "{{ door_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:
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 }}
door_message: >-
{{ trigger.to_state.attributes.friendly_name |
default(trigger.entity_id.split('.')[-1]) }} ouverte

On a ainsi un système totalement opérationnel, mais le but ultime n'est pas là. Il est d'avoir une automation de présence pièce / choix de Google Home / Nest et diffusion de message qui pourra être réutilisé par toutes les autres automations.
Tout déclencheur activé provoque la génération d'un message, et tous les messages sont stockés en mode queue et diffusés les uns à la suite des autres.
Par exemple :
- Vous ouvrez 5 fenêtres dans une pièce en étant présent dans cette pièce : le 1er et le 2ᵉ message sont joués dans cette pièce.
- Vous changez de pièce, le 3e message sera peut-être joué dans la pièce d'où vous sortez (ça dépend du temps de remontée de la détection, les esp32 +2410 sont vraiment très performants pour cet usage), mais il sera à coup sûr joué dans la pièce où vous venez d'être détecté...
C'est vraiment très efficace, pour peu qu'il n'y ait pas de latence de détection ( comme avec de nombreux modules de mouvement, qui vous gardent "présent" pendant 1 minute environ).
Et si on mutualisait le concept ?
Une seule pour les gouverner toutes et dans ma domotique les lier...
On a donc le "diffuseur contextuel", qui se charge de régler le volume et de diffuser le message dans la ou les pièces où se trouve quelqu'un.
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:
- data:
title: TTS Diffusé
message: 🔈 {{ message }} → {{ occupied_rooms | join(', ') }}
notification_id: tts_{{ now().timestamp() | int }}
action: persistent_notification.create
- 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 }}
À noter que
triggers:
- event_type: dummy_startup_never_fired
trigger: event
Ou équivalent, ne sert à rien, mais est obligatoire pour le bon fonctionnement.
L'ajout d'une notification permanente de DEBUG, validera que tout est en place.
- data:
title: TTS Diffusé
message: 🔈 {{ message }} → {{ occupied_rooms | join(', ') }}
notification_id: tts_{{ now().timestamp() | int }}
action: persistent_notification.create
On supprimera cette partie si tout fonctionne.

On complètera avec toutes les automations de détection (ou autre) que l'on voudra utiliser.
Ici surveillance des ouvrants :
alias: TTS Porte ouverte - déclencheur
description: Détecte une ouverture de porte et déclenche la diffusion contextuelle
triggers:
- entity_id:
- binary_sensor.porte_entree_2
- binary_sensor.porte_chaufferie_labopi2
- binary_sensor.porte_labo_labopi2
from: "off"
to: "on"
trigger: state
actions:
- data:
skip_condition: true
variables:
message: |-
{{ trigger.to_state.attributes.friendly_name
| default(trigger.entity_id.split('.')[-1].replace('_', ' ')) }} ouverte
target:
entity_id: automation.tts_diffuseur_contextuel
action: automation.trigger
mode: queued
Ou la surveillance des présences.
alias: TTS Presence - déclencheur
description: Détecte une presence et déclenche la diffusion contextuelle
triggers:
- entity_id:
- binary_sensor.esp1_entree_radar_target
- binary_sensor.esp2_entree_radar_target
- binary_sensor.esp1_chaufferie_radar_target
- binary_sensor.esp2_chaufferie_radar_target
- binary_sensor.esp1_labo_radar_target
- binary_sensor.mvt_wc_occupancy
from: "off"
to: "on"
trigger: state
actions:
- data:
skip_condition: true
variables:
message: |-
{{ trigger.to_state.attributes.friendly_name
| default(trigger.entity_id.split('.')[-1].replace('_', ' ')) }} detectee
target:
entity_id: automation.tts_diffuseur_contextuel
action: automation.trigger
mode: queued
En bonus, voici une partie du centre de notification de mon HA pour être averti que quelqu'un sonne (c'est la partie data en fin de code qui nous intéresse surtout) pour les messages simples.
choose:
- conditions:
- condition: trigger
id: Sonette
sequence:
- sequence:
- parallel:
- data:
message: 🔔 Quelqu'un sonne ({{ now().strftime('%d/%m/%y %Hh%M') }})
action: telegram_bot.send_message
- action: notify.freetronichabot
metadata: {}
data:
message: 🔔 Quelqu'un sonne ({{ now().strftime('%d/%m/%y %Hh%M') }})
target:
- "xxx"
- metadata: {}
data:
filename: /config/www/photos_cameras/tmp_lettres0.jpg
target:
entity_id: camera.sonnette_fluent
action: camera.snapshot
- parallel:
- data:
authentication: digest
file: /config/www/photos_cameras/tmp_lettres0.jpg
action: telegram_bot.send_photo
- action: notify.freetronichabot
metadata: {}
data:
message: ""
target:
- "xxx"
data:
images:
- /config/www/photos_cameras/tmp_lettres0.jpg
- data:
skip_condition: true
variables:
message: Quelqu'un sonne
target:
entity_id: automation.tts_diffuseur_contextuel
action: automation.trigger

Ou tout ce qu'on veut d'autre.
Bilan de cette deuxième implémentation
Ce système offre dans sa version finale un centre de notification sur tous les media_player de la maison, jouant uniquement dans les pièces où une présence est détectée. Il peut être utilisé par toutes les autres automations qui le déclenchent et apportent à mon sens un vrai plus à la maison connectée.
Conclusion finale
À chacun son approche, donc à vous de choisir ce qui vous convient le mieux, les scripts ou les automations, au final tant que ça fonctionne...
Remerciements à @Gael qui a posé les bases de tout le reste, merci aux IA, Claude, Chatgpt, Copilot, qui m'ont largement aidé dans cette réalisation, à mon clavier qui a supporté ces heures de saisie informatique ;)
Comme le dit @Freetronic, à chacun son approche. Pour nous deux la présence se base sur LD2410 sur esp32 dans esphome. Elle pourrait tout aussi bien être gérée avec des proxyBT et l’extension Bermuda BLE Trilateration pour suivre une montre connecté.
Pour les lecteurs multimédia par contre chacun son approche. Mais ici aussi rien n'est figé, tout lecteur capable de recevoir du TTS fera l'affaire.
L'un comme l'autre, nous en avons sué pour en arriver là, en échangeant nos idées, (qui a dit "à la con" ?). Maintenant, à vous de jouer et venez partager vos réalisations.