Objectif : rendre visibles (et exploitables) deux infos clés pour chaque device Zigbee :
- linkquality (santé radio) et
- last_seen (dernière communication)
Cela afin de repérer immédiatement une sirène “perdue” ou une batterie HS, sans attendre de le découvrir par hasard.

🧭 Pourquoi j’ai mis ça en place
Avec 69 appareils Zigbee, je n’avais pas de visibilité simple pour répondre à deux questions basiques : est-ce que l’appareil capte correctement ? et est-ce qu’il a communiqué récemment ?
Sur une sirène, ce n’est pas acceptable de le découvrir “au hasard” : je veux voir immédiatement si elle est muette, hors réseau, ou en difficulté radio.
J’ai donc décidé d’afficher partout :
- linkquality : indicateur de qualité du lien radio (LQI). (C’est une “photo” utile pour repérer les zones faibles.)
- last_seen : date/heure de la dernière communication (ou un temps relatif : 12s, 4m, 2h…).
⚠️ Piège #1 : il y a deux “configuration.yaml”
Au début, je me suis fait avoir : dans Home Assistant, il y a bien deux fichiers différents.
- Le fichier principal de Home Assistant : /config/configuration.yaml
- Le fichier de Zigbee2MQTT : /config/zigbee2mqtt/configuration.yaml
Important : pour last_seen et (en pratique) l’exposition/activation par défaut, tout se configure côté Zigbee2MQTT, donc dans /config/zigbee2mqtt/configuration.yaml.

🧹 Piège #2 : l’UI de l’add-on Zigbee2MQTT peut “nettoyer” des options

J’ai eu un comportement très pénible : j’ajoutais des options avancées, je sauvegardais… puis l’interface de l’add-on semblait “simplifier” le fichier en supprimant ce qu’elle ne comprenait pas.
La méthode fiable (chez moi) :
- J’édite directement
/config/zigbee2mqtt/configuration.yaml(File Editor ou SSH) - Je sauvegarde
- Je redémarre Zigbee2MQTT
🕒 Activer last_seen dans Zigbee2MQTT (obligatoire)
Par défaut, Zigbee2MQTT peut ne pas publier last_seen (valeur “disable”).
Pour l’activer, j’ajoute ceci dans la section advanced de /config/zigbee2mqtt/configuration.yaml :
advanced: # Ajoute "last_seen" dans les messages MQTT # valeurs possibles: disable, ISO_8601, ISO_8601_local, epoch last_seen: ISO_8601_local
Ce réglage indique à Zigbee2MQTT d’ajouter l’attribut last_seen aux messages MQTT, avec un format lisible en heure locale.
Formats utiles (pour choisir)
- ISO_8601_local : lisible + fuseau local (le plus pratique au quotidien).
- ISO_8601 : lisible, mais en UTC (utile pour centraliser tout en UTC).
- epoch : timestamp Unix (pratique pour scripts/traitements).
🧩 Activer last_seen et linkquality “par défaut” dans Home Assistant (facultatif)

Une fois last_seen activé côté Zigbee2MQTT, il reste un point : dans Home Assistant, ces entités sont souvent classées “diagnostic”, donc elles peuvent être créées mais désactivées automatiquement.
Pour que chaque nouvel appareil Zigbee ajouté arrive directement “prêt à superviser”, j’ajoute ceci dans /config/zigbee2mqtt/configuration.yaml :
device_options:
homeassistant:
last_seen:
enabled_by_default: true
linkquality:
enabled_by_default: true
Puis je redémarre Zigbee2MQTT.
Important : impact sur les appareils déjà installés
Ce réglage aide surtout les nouveaux appareils (ou les appareils redécouverts).
Pour les devices déjà présents, il faut souvent activer manuellement les entités correspondantes dans Home Assistant.
🗄️ Base de données : garder l’info visible sans “polluer” l’historique
Le “risque” n’est pas tellement Zigbee2MQTT (il ajoute juste un champ timestamp dans les messages), mais plutôt Home Assistant si on enregistre chaque update de last_seen dans la base : ça peut générer beaucoup d’écritures avec beaucoup de capteurs.
Mon choix : je veux voir ces valeurs partout, mais je n’ai pas besoin de graphes minute par minute. J’exclus donc ces entités du recorder.
Ce que je mets dans /config/recorders.yaml
recorder:
exclude:
entity_globs:
- sensor.*_last_seen
- sensor.*_linkquality
Après ça, je redémarre Home Assistant : les valeurs restent visibles en temps réel, mais je limite fortement la création d’historique inutile.
🗺️ Supervision visuelle : un plan “radio + fraîcheur”
Je ne voulais pas “une entité perdue dans une liste” : je voulais une supervision visuelle, immédiatement lisible.
Principe :
- Chaque appareil Zigbee apparaît sous forme d’un petit rectangle sur un plan.
- Le nombre affiché en gros = Linkquality.
- Le temps en bas à droite = Last_seen en relatif (s / m / h / d).
- Le cadre noir = routeur (variable router: 1), pour reconnaître vite les routeurs.

🧰 La carte “decluttering” (réutilisable)
J’utilise la magie de la carte decluttering_templates que j’avais décrite dans cette page il y a 3 ans :
Ainsi decluttering_templates + custom:decluttering-card pour réutiliser le même composant partout, en ne changeant que 4 variables.
Je défini tout simplement 4 éléments qui sont différents pour chaque appareils :
- zone: c’est le capteur qui va être traité
- top: sa position depuis le haut
- left: sa position depuis la gauche
- router: 1 si c’est un routeur sinon 0
Exemple : page Dashboard très simple
type: picture-elements
image: /local/maison/1920x1000/off_0800.png
elements:
- type: custom:decluttering-card
template: linkquality
variables:
- zone: sensor.lumiere_chambre_parents
- top: 20%
- left: 35%
- router: 1
- type: custom:decluttering-card
template: linkquality
variables:
- zone: sensor.fenetre_de_la_chambre_des_parents
- top: 15%
- left: 37%
- router: 0
Le template linkquality (le “gros morceau”)
decluttering_templates:
linkquality:
default:
- top: 5%
- left: 95%
- router: 0
element:
type: custom:button-card
entity: |
[[[
return `[[zone]]_linkquality`;
]]]
show_icon: false
name: |
[[[
return '[[zone]]'
.replace(/^sensor\\./i, '')
.replace(/_/g, ' ')
.trim();
]]]
show_state: true
show_units: false
custom_fields:
lastseen: |
[[[
const obj = states[`[[zone]]_last_seen`];
if (!obj || !obj.state || obj.state === 'unavailable') return '!!';
const last = new Date(obj.state);
if (isNaN(last.getTime())) return '!!';
const now = new Date();
const seconds = Math.floor((now - last) / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
return `${days}d`;
]]]
styles:
card:
- border: |
[[[ return ([[router]] == 1) ? '2px solid black' : 'none'; ]]]
- box-shadow: none
- position: relative
- border-radius: 8px
- min-width: 100px
- height: 45px
- display: flex
- flex-direction: column
- justify-content: center
- align-items: center
- color: white
name:
- font-size: 10px
- line-height: '1'
- padding: 0
- margin: 0
- text-align: center
- color: white
- flex: 1
- max-width: 95px
- word-wrap: break-word
- overflow: hidden
- white-space: normal
state:
- font-size: 15px
- font-weight: bold
- line-height: '1'
- padding: 0
- margin: 0
- text-align: center
- color: white
- flex: 1
custom_fields:
lastseen:
- position: absolute
- bottom: 0px
- right: 0px
- font-size: 10px
- color: black
- padding: 2px 4px
- border-radius: 4px
- box-shadow: 0 1px 3px rgba(0,0,0,0.3)
- background: |
[[[
const obj = states[`[[zone]]_last_seen`];
if (!obj || !obj.state) return 'rgba(128,128,128,0.5)';
const last = new Date(obj.state);
const now = new Date();
const seconds = Math.floor((now - last) / 1000);
if (seconds < 1200) return 'rgba(0,255,0,0.9)'; // Vert <20min
if (seconds < 1800) return 'rgba(255,255,0,0.9)'; // Jaune 20-30min
if (seconds < 7200) return 'rgba(255,165,0,0.9)'; // Orange <2h return 'rgba(255,0,0,0.9)'; // Rouge >2h
]]]
state:
- value: unavailable
styles:
card:
- background: rgba(255,0,0,0.3)
- operator: '>'
value: 200
styles:
card:
- background: rgba(0,180,0,0.3)
- operator: '>'
value: 150
styles:
card:
- background: rgba(34,255,34,0.3)
- operator: '>'
value: 130
styles:
card:
- background: rgba(255,255,0,0.3)
- operator: '>='
value: 80
styles:
card:
- background: rgba(255,165,0,0.3)
- operator: '<'
value: 80
styles:
card:
- background: rgba(255,107,53,0.3)
style:
top: '[[top]]'
left: '[[left]]'
Simplement, que fait ce code ?
Ce template définit un modèle unique de carte que tu réutilises pour tous tes appareils Zigbee, en ne changeant que la variable [[zone]].
La propriété entity est construite dynamiquement en JavaScript ([[[ … ]]]) pour pointer vers l’entité [[zone]]_linkquality, ce qui permet d’afficher automatiquement la qualité de lien du device (valeur LQI remontée par Zigbee2MQTT ou ZHA).
Le name est lui aussi généré côté client : on retire le préfixe sensor. puis on remplace les _ par des espaces et on fait un trim(), ce qui produit un libellé lisible à partir de l’ID de l’entité.
Le champ personnalisé lastseen lit l’entité [[zone]]_last_seen dans states[], convertit sa valeur en objet Date, puis calcule la différence avec l’heure actuelle pour afficher un temps relatif (s, m, h, d). Si la valeur est vide, unavailable ou une date invalide, il affiche !! au lieu de laisser apparaître une valeur incohérente.
Le style du badge lastseen en bas à droite utilise une fonction JavaScript qui renvoie une couleur RGBA différente selon l’âge de la dernière communication (gris si pas de valeur, puis vert → jaune → orange → rouge en fonction du nombre de secondes écoulées), ce qui encode visuellement la fraîcheur des données.
La section state: du button-card définit plusieurs seuils sur la valeur de LinkQuality : indisponible, puis tranches >200, >150, >130, >=80, <80, chacune appliquant un fond coloré spécifique à la carte, ce qui transforme une valeur numérique en indicateur de qualité radio immédiatement lisible.
Les propriétés de mise en forme (flex, min-width, height, border-radius, position absolute du badge, etc.) assurent un rendu compact et uniforme, avec une bordure noire optionnelle quand [[router]] == 1 pour identifier visuellement les routeurs Zigbee par rapport aux simples terminaux.
🧠 Ajouts utiles : infos “santé” globales Zigbee2MQTT

Voici le code de cette carte
type: picture-elements
image: /local/maison/1920x1000/off_0800.png
elements:
- type: custom:mod-card
card:
type: entities
title: Zigbee2MQTT
show_header_toggle: false
entities:
- entity: switch.zigbee2mqtt_bridge_permit_join
name: Autoriser inclusion
- entity: button.zigbee2mqtt_bridge_restart
name: Redémarrer
- entity: select.zigbee2mqtt_bridge_log_level
name: Niveau de log
- entity: binary_sensor.zigbee2mqtt_bridge_connection_state
name: État de connexion
- type: custom:template-entity-row
name: Lancé depuis
state: >
{% if states('binary_sensor.zigbee2mqtt_bridge_connection_state')
not in ['unavailable', 'unknown'] %}
{{ ((now() - states('binary_sensor.zigbee2mqtt_bridge_connection_state').last_changed).total_seconds() / 3600) | round(1) }} h
{% else %}
Non disponible
{% endif %}
- entity: sensor.zigbee2mqtt_bridge_coordinator_version
name: Version coordinateur
- entity: sensor.zigbee2mqtt_bridge_version
name: Version Zigbee2MQTT
- entity: sensor.nombre_de_capteurs_linkq_en_erreur
name: Injoignables
icon: mdi:alert-circle
- entity: sensor.nombre_de_capteurs_linkq
name: Total
icon: mdi:devices
- entity: sensor.zigbee_linkquality_moyenne
name: Moyenne LinkQuality
icon: mdi:signal
style:
top: 35%
left: 75%
width: 450px
J’ai ajouté un capteur pour calculer la moyenne de Linkquality
![]()
Ce capteur s’ajoute dans le fichier templates.yaml, voici son code :
- name: "zigbee_linkquality_moyenne"
unique_id: zigbee_linkquality_moyenne
unit_of_measurement: "LQI"
state_class: measurement
state: >-
{% set linkqualities = states.sensor
| selectattr('entity_id', 'search', '_linkquality')
| selectattr('state', 'is_number')
| map(attribute='state') | map('float') | list %}
{% if linkqualities | length > 0 %}
{{ (linkqualities | sum / linkqualities | length) | round(1) }}
{% else %} 0 {% endif %}
J’ai ajouté un capteur pour calculer le nombre de capteurs
![]()
- name: "Nombre de capteurs Linkq"
unique_id: sensor.nombre_capteurs_linkq
state: >-
{% set lq_sensors = states.sensor
| selectattr('entity_id', 'search', '_linkquality$')
| list %}
{{ lq_sensors | count }}
icon: mdi:signal-variant
unit_of_measurement: "capteurs"
J’ai ajouté un capteur pour calculer le nombre d’appareils en erreur
![]()
- name: "Nombre de capteurs Linkq en erreur"
unique_id: sensor.nombre_capteurs_linkq_erreur
state: >-
{% set lq_sensors = states.sensor
| selectattr('entity_id', 'search', '_linkquality$')
| selectattr('state', 'in', ['unavailable', 'unknown', 'None'])
| list %}
{{ lq_sensors | count }}
icon: mdi:alert-circle
unit_of_measurement: "capteurs"
J’ai ajouté un indicateur sur le dasboard principal pour signaler le nombre en erreur

Tout d’abord ce template pour afficher xx en erreur / yy au total
- name: "zigbee_injoignables_synthese"
unique_id: sensor.zigbee_injoignables_synthese
state: >-
{{ states('sensor.nombre_de_capteurs_linkq_en_erreur') }}/{{ states('sensor.nombre_de_capteurs_linkq') }}
icon: mdi:alert-circle
Et dans mon picture-elements, j’ajoute :
- type: conditional
conditions:
- condition: state
entity: sensor.nombre_de_capteurs_linkq_en_erreur
state_not: "0"
elements:
- type: custom:button-card
entity: sensor.zigbee_injoignables_synthese
icon: mdi:zigbee
show_name: false
show_state: true
style:
top: 5%
left: 5%
transform: translate(-50%, -50%)
styles:
state:
- color: red
- font-weight: 700
- font-size: 16px
icon:
- color: red
- width: 20px
- height: 20px
card:
- border-radius: 999px
- padding: 4px 6px
- background: transparent
- box-shadow: none
Ce bloc sert à afficher un petit indicateur rouge « Zigbee en erreur » sur le dashboard plan uniquement quand il y a au moins un capteur problématique.
C’est un élément conditional dans une carte picture-elements : il ne montre son contenu que si sensor.nombre_de_capteurs_linkq_en_erreur est différent de « 0 » (donc si au moins un capteur est injoignable ou en erreur de LinkQuality).
Quand la condition est vraie, il affiche un custom:button-card basé sur sensor.zigbee_injoignables_synthese, qui contient probablement un texte du style « 3 en erreur » ou similaire.
Le bouton est placé en haut à gauche du plan (5 % / 5 %), centré grâce au transform: translate(-50%, -50%), avec un fond transparent et sans ombre pour ne garder que l’icône Zigbee et le texte.
L’icône Zigbee et l’état sont stylés en rouge, en gras et en taille 16 px, avec un border-radius: 999px pour donner un aspect de pastille/étiquette, ce qui en fait un avertissement visuel discret mais bien visible tant qu’il existe des capteurs en erreur.
Pour savoir qui est en erreur

Pour avoir d’un coup d’œil la liste des équipements en erreur, voici les codes nécessaires.
La déclaration des templates
- name: "Nombre de capteurs Linkq en erreur"
unique_id: sensor.nombre_capteurs_linkq_erreur
state: >-
{% set lq_sensors = states.sensor
| selectattr('entity_id', 'search', '_linkquality$')
| selectattr('state', 'in', ['unavailable', 'unknown', 'None'])
| list %}
{{ lq_sensors | count }}
icon: mdi:alert-circle
unit_of_measurement: "capteurs"
- name: "Liste capteurs Linkquality en erreur"
unique_id: sensor.liste_capteurs_linkquality_erreur
state: >-
{% set lq_sensors = states.sensor
| selectattr('entity_id', 'search', '_linkquality$')
| selectattr('state', 'in', ['unavailable', 'unknown', 'None'])
| map(attribute='entity_id')
| map('regex_replace', 'sensor\\\\\\\\.(.*)_linkquality$', '\\\\\\\\1')
| list %}
{% set count = lq_sensors | length %}
{% if count == 0 %}
Aucun
{% elif count <= 5 %}
{{ lq_sensors | join(', ') }}
{% else %}
{{ lq_sensors[:5] | join(', ') ~ ' + ' ~ count }}
{% endif %}
attributes:
count: >-
{% set lq_sensors = states.sensor
| selectattr('entity_id', 'search', '_linkquality$')
| selectattr('state', 'in', ['unavailable', 'unknown', 'None'])
| map(attribute='entity_id')
| map('regex_replace', 'sensor\\\\\\\\.(.*)_linkquality$', '\\\\\\\\1')
| list %}
{{ lq_sensors | length }}
full_list: >-
{% set lq_sensors = states.sensor
| selectattr('entity_id', 'search', '_linkquality$')
| selectattr('state', 'in', ['unavailable', 'unknown', 'None'])
| map(attribute='entity_id')
| map('regex_replace', 'sensor\\\\\\\\.(.*)_linkquality$', '\\\\\\\\1')
| list %}
{{ lq_sensors | join(', ') | default('Aucun') }}
icon: mdi:alert-circle
Et sur le Dasboard, toujours dans le picture-elements
type: picture-elements
image: /local/maison/1920x1000/off_0800.png
elements:
- type: custom:mod-card
card:
type: entities
title: Capteurs Zigbee en erreur
entities:
- type: custom:template-entity-row
entity: sensor.liste_capteurs_linkquality_en_erreur
name: Liste
state: >
{% set s = states('sensor.liste_capteurs_linkquality_en_erreur')
%} {% if s not in ['unknown', 'unavailable', ''] %}
{{ s.replace(',', '\\\\n') }}
{% else %}
Aucun
{% endif %}
style:
top: 80%
left: 75%
width: 450px
✅ Récapitulatif simple (les 3 blocs indispensables)
1. Zigbee2MQTT : activer last_seen
advanced: last_seen: ISO_8601_local
2. Zigbee2MQTT : activer par défaut dans HA (optionnel mais pratique)
device_options:
homeassistant:
last_seen:
enabled_by_default: true
linkquality:
enabled_by_default: true
3. Home Assistant : ne pas enregistrer ces entités dans la DB
recorder:
exclude:
entity_globs:
- sensor.*_last_seen
- sensor.*_linkquality
🎯 Ce que ça change au quotidien
- Je vois immédiatement les zones faibles (linkquality bas) et je sais quoi renforcer (routeur, placement, canal, etc.).
- Je sais exactement quand une sirène a cessé de parler (last_seen), donc je ne “suppose” plus.
- Je garde une base de données saine (pas d’historique inutile sur des capteurs qui bougent tout le temps).
Besoin de précisions ? des questions ?
Cette discussion (si elle est encore ouverte) est disponible :


