Notifications dynamiques en fonction de la pièce occupée

Vous avez plusieurs assistants vocaux et ne voulez pas qu'ils se déclenchent systématiquement dans toutes les pièces ? Voici comment gérer des notifications intelligentes dans la maison en fonction de la présence ou non des personnes dans les différentes pièces.
Notifications dynamiques en fonction de la pièce occupée

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) :

Multi-capteurs DYI : radar de présence, température, lumière et bien plus
Voici comment créer avec ESP Home un multi-capteur à base de microcontrôleur ESP, intégrant un radar de présence et différents autres capteurs. Nous aborderons aussi l’utilisation de ces capteurs pour créer un système de maillage pour assurer une couverture Bluetooth dans tout votre logement.

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é :

  1. Salon
  2. Cuisine
  3. Chambre
  4. 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.

Open your Home Assistant instance and show the add-on store.

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 :

  1. Contexte : "Génère un message vocal pour prévenir que le café est prêt".
  2. Personnalité : Style K-2SO avec humour/sarcasme léger.
  3. Contraintes techniques : Adapté au TTS, sans émoticônes.
  4. Exemples : 4 exemples concrets du ton attendu.
  5. 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 :

  1. 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).
  2. Localisation : Un sensor template détermine la pièce occupée.
  3. Génération : un message est généré
  4. 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 bloc if
  • 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 ou esp2) détecte une présence (== 'on'), alors l'état du capteur devient "Entree", sinon il reste vide ('').
  • Attribut echo : liste des media_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 ajoute media_player.entree à la liste rooms.

Ce schéma est répété pour chaque pièce :

  • sensor.presence_cuisinemedia_player.cuisine
  • sensor.presence_labomedia_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.