Sommaire
Je partage avec vous mon projet de chatière connectée, entièrement intégrée à Home Assistant, qui me permet de savoir à tout moment si mes chats (Kiwi et Maya) sont à l’intérieur ou à l’extérieur de la maison. Le système est basé sur la lecture de leur puce électronique vétérinaire implantée dans le cou, de type “FDX-B”.
Ma motivation pour ce projet
Mes deux chats sont libres de rentrer ou sortir de la maison comme ils le souhaitent, via une chatière. Lorsque je suis absent ou en vacances, je veux m’assurer que les chats sont là, et savoir qui est à la maison et qui est dehors. Les projets de ce type qui existent déjà utilisent tous une puce RFID attachée à un collier. Mais cette option n'est pas envisageable pour moi, car mes chats sortent souvent, et le risque d’étranglement est réel.
Il y a quelques années, j’avais acheté une chatière de marque “Sureflap”, qui lit les puces FDX-B implantées des chats, mais j’ai dû la retourner car la puce de l’un de mes deux chats n’était presque jamais détectée.
Après plusieurs semaines de réflexion et de recherches infructueuses en ligne, j’ai finalement réussi à concevoir ma propre chatière intelligente.
Matériel nécessaire
Voici la liste complète du matériel que j’ai utilisé pour ce projet :
- Module RFID WL134 (FDX-B / EM4100 / ISO11784/11785)
C’est le cœur du système : il lit directement les puces de vos chats. L’antenne est fournie avec le module. J’ai essayé de fabriquer une antenne plus large pour entourer toute la chatière, mais cela ne marche pas : la sensibilité chute trop.
Le meilleur résultat est obtenu en collant l’antenne incluse sur la porte intérieure de la chatière. - Alimentation linéaire 6 V DC
Quand je l’ai achetée, elle était moins chère… Peut-être il vous faut trouver une autre référence, mais évitez absolument les alimentations à découpage premier prix (j’y reviens plus tard) - Convertisseur DC-DC
Pour convertir le 6V en 5V et alimenter le microcontrôleur (ESP32) à partir de la même source. On évite ainsi les multi-blocs de prises. - Accéléromètre MPU6050
Pour connaître le sens dans lequel s'ouvre la chatière - Module ESP32
Pour envoyer les données du lecteur RFID vers Home Assistant via MQTT. - Adaptateur DC 5.5 x 2.1 mm, bien que j’aurais pu couper le câble et directement faire mes soudures mais c’est moins joli
Ce que j’avais déjà, mais dont vous aurez besoin : des fils électriques fins de type AWG28 en silicone, un fer à souder et de l’étain, un multimètre, deux petits boitiers pour la finition.
Optionnel : quelques puces RFID pour tester parce qu’à un moment le chat n’était plus trop coopératif lors de mes tests et en avait marre de mon antenne 😺
Expériences et erreurs à éviter
L’alimentation du module WL-134 est la clé de la réussite, car elle est très sensible aux interférences. Au départ, j’ai essayé de l’alimenter avec un bloc 9 V classique, couplé à des résistances et transistors pour réguler la tension. Résultat : un week-end entier de perdu, la lecture était trop aléatoire.
J’ai demandé une documentation au vendeur du module, que je mets à disposition ici. Il explique qu’il faut une alimentation linéaire 6 V stable délivrant environ 80 mA pour que le WL-134 lise correctement les puces FDX-B. Le document nous apprend aussi qu’on peut alimenter le module avec une batterie Li-ion (entre 5V et 9V) et que dans ce cas les interférences sont quasi nulles. Je n’ai personnellement pas essayé la méthode de la batterie, mais je pense que ça serait une méthode beaucoup plus économique.
Il existe peut-être de meilleures méthodes pour alimenter le module, ou des alimentations moins chères qui n’enverront pas d’interférences, mais je n'ai pas eu l'occasion de les tester. Vous pouvez toujours étudier le fichier joint et décider de l’alimentation pour laquelle vous allez opter.
Schéma de branchement
Un schéma est toujours plus parlant que des mots, voici les soudures ou branchements à effectuer :

Quelques précisions :
- Il faut bien relier le RX2 du ESP32 sur la borne 3.3vTX du WL-134 et pas sur la borne 5vTX sinon c’est l’assurance de griller l’ESP32.
- Branchement du MPU6050 :
3V3 ESP32 vers VCC MPU6050
GND ESP32 vers GND MPU6050
GPIO22 ESP32 vers SCL MPU6050
GPIO21 ESP32 vers SDA MPU6050

Mise en situation :


L’accéléromètre est collé en haut de la chatière, sens horizontal, soudures vers le haut et LED à l’extérieur.
Calibrage du WL-134
Le WL-134 doit être calibré pour que l’antenne soit en parfaite résonance.

Le module a 5 cavaliers J1 à J5 qui règlent la capacité de résonance de l’antenne. Si vous achetez la même alimentation que la mienne, vous pouvez ignorer cette étape. Vérifiez juste que les cavaliers J4 et J5 sont reliés (soudés) et que les cavaliers J1 à J3 sont libres (dessoudés), car j’ai remarqué que c’est la meilleure configuration avec cette alim.
Si vous utilisez une autre alimentation ou voulez mieux régler la résonance de l’antenne, voici comment procéder. Un WL-134 bien réglé consomme environ 80 mA à 6 V et lit les puces à plusieurs centimètres. Un mauvais réglage se traduit par une consommation beaucoup plus faible (< 50 mA) et une portée réduite.
- Coupez l’alimentation. On garde les branchements précédents (on déconnecte l’esp32 par précaution), mais on va insérer le multimètre en série sur le fil +6V, afin qu’il devienne un « pont » entre l’alim et le module WL-134.
Multimètre réglé sur mA DC. Borne mA rouge du multimètre à la sortie +6V de l’alimentation et borne noire COM du multimètre sur l’entrée+5-9V du WL-134.
Rebranchez l’alimentation - Dessoudez tous les cavaliers (J1 à J5 ouverts). Notez le courant de départ (par ex. 55 mA)
- Remettez J5 (ressoudez) et observez si le courant augmente ou diminue. S’il augmente, gardez J5 : vous allez dans la bonne direction. S’il baisse, retirez le et passez à J4.
- Continuez ainsi (J4, puis J3, etc.) en testant chaque combinaison.
- L’objectif est d’obtenir la consommation la plus élevée possible (souvent entre 75 et 85 mA à 6 V). Si le courant dépasse 100 mA, c’est que la résonance est trop forte (capacité trop élevée) ; retirez alors un cavalier
Le code à injecter dans l’ESP32
L’ESP lit les trames envoyées par le WL-134 et les publie dans Home Assistant. Le MPU6050 permet de savoir si la chatière s'ouvre ou se ferme selon l'inclinaison du module.
Dans Home Assistant, un sensor permet ensuite d’afficher le nom du chat détecté, l'état du clapet de la chatière, et de déclencher des automatisations (notifications Telegram, journal des passages, etc.).
Je ne vais pas étendre ce tuto au paramétrage d’un ESP32, mais basiquement, vous le branchez à votre PC, vous installez les drivers, et vous y injectez le code yaml ci-dessous. Il vous faudra le module “ESPHome Device Builder” dans Home Assistant.
Collez le code ci-après, n’oubliez pas d’éditer le SSID wifi et le mot de passe en fonction de votre propre réseau (lignes 16 et 17). Branchez l’ESP32 et incluez-le dans Home Assistant. Normalement, il va apparaitre tout seul dans “appareils et services” et il suffira de l’ajouter à votre HA. Pour le moment ce code ne prend pas en compte l'accéléromètre.
esphome:
name: lecteur-chatiere
on_boot:
priority: -10
then:
- text_sensor.template.publish:
id: rfid_chat
state: ""
esp32:
board: nodemcu-32s
framework:
type: arduino
wifi:
ssid: "VOTRE RESEAU WIFI"
password: "VOTRE MOT DE PASSE WIFI"
power_save_mode: none
api:
reboot_timeout: 0s
ota:
platform: esphome
uart:
id: rfid_uart
rx_pin: GPIO16
baud_rate: 9600
data_bits: 8
parity: NONE
stop_bits: 2
text_sensor:
- platform: template
name: "RFID UID"
id: rfid_uid
icon: "mdi:identifier"
interval:
- interval: 100ms
then:
- lambda: |-
static std::string buf;
static uint32_t last = 0;
// Lire tout ce qui arrive
while (id(rfid_uart).available()) {
uint8_t b; id(rfid_uart).read_byte(&b);
buf.push_back((char)b);
last = millis();
}
// Traiter après ~120 ms de silence
if (!buf.empty() && (millis() - last) > 120) {
// Chercher STX(0x02) ... ETX(0x03)
auto stx = buf.find('\x02');
auto etx = buf.rfind('\x03');
if (stx != std::string::npos && etx != std::string::npos && etx > stx + 1) {
// Charge utile entre STX et ETX, retirer tout après 0x7E s'il existe
std::string payload = buf.substr(stx + 1, etx - (stx + 1));
auto tilde = payload.find('\x7E');
if (tilde != std::string::npos) payload = payload.substr(0, tilde);
// On attend 26 caractères hex ASCII (ex: "5460D5C3D3483011000000000000")
if (payload.size() >= 16) {
// Prendre les 16 premiers caractères = UID (8 octets)
std::string uid_hex = payload.substr(0, 16);
// Anti-doublon 5 s
static std::string last_uid;
static uint32_t last_uid_ms = 0;
if (uid_hex != last_uid || (millis() - last_uid_ms) > 5000) {
id(rfid_uid).publish_state(uid_hex.c_str());
last_uid = uid_hex;
last_uid_ms = millis();
}
}
}
buf.clear();
}
Faites passer une puce RFID type FDX-B (ou votre chat) devant l’antenne. Une LED sur le WL-134 devient bleue si la puce est détectée.
Regardez dans Home Assistant les caractères qui sont lus. Vous devriez voir un code d’identification au format UID hex, composé de 16 caractères
Une fois que vous avez les codes de tous vos animaux, vous pouvez demander à Home Assistant de noter le nom du chat plutôt que le code hex. C’est plus explicite que de remonter un numéro. Pour cela, remplacez le code précédent par celui ci-dessous.
Vous devez à nouveau éditer les paramètres WIFI (lignes 19 et 20) et éditer les noms de vos chats lignes 299 et 300 avec leurs identifiants trouvés précédemment.
esphome:
name: lecteur-chatiere
on_boot:
priority: -10
then:
- text_sensor.template.publish:
id: rfid_chat
state: ""
- text_sensor.template.publish:
id: sens_chatiere
state: "Fermée"
esp32:
board: nodemcu-32s
framework:
type: arduino
wifi:
ssid: "VOTRE SSID WIFI"
password: "MOT DE PASSE WIFI"
power_save_mode: none
reboot_timeout: 15min
logger:
level: INFO
baud_rate: 0
logs:
sensor: WARN
text_sensor: WARN
component: ERROR
api:
reboot_timeout: 0s
ota:
platform: esphome
globals:
- id: last_closed_z
type: float
restore_value: no
initial_value: '0.0'
- id: last_peak_z
type: float
restore_value: no
initial_value: '0.0'
time:
- platform: sntp
id: sntp_time
timezone: Europe/Paris
servers: [0.pool.ntp.org, 1.pool.ntp.org, 2.pool.ntp.org]
on_time:
- seconds: 0
minutes: 0
hours: 3
then:
- logger.log: "🔄 Reboot quotidien (03:00 Europe/Paris)"
- delay: 1s
- lambda: |-
esp_restart();
# Configuration I2C pour le MPU-6050
i2c:
sda: GPIO21
scl: GPIO22
scan: true
id: bus_a
# Configuration UART pour le lecteur RFID
uart:
id: rfid_uart
rx_pin: GPIO16
baud_rate: 9600
data_bits: 8
parity: NONE
stop_bits: 2
# Capteur MPU-6050
sensor:
- platform: mpu6050
address: 0x68
i2c_id: bus_a
update_interval: 100ms
accel_x:
name: "Chatière Accel X"
id: accel_x
internal: true
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
accel_y:
name: "Chatière Accel Y"
id: accel_y
internal: true
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
accel_z:
name: "Chatière Accel Z brute"
id: accel_z
internal: true
filters:
# Inverse le signe pour corriger l'orientation mécanique
- multiply: -1
- sliding_window_moving_average:
window_size: 5
send_every: 1
gyro_x:
name: "Chatière Gyro X"
id: gyro_x
internal: true
gyro_y:
name: "Chatière Gyro Y"
id: gyro_y
internal: true
gyro_z:
name: "Chatière Gyro Z"
id: gyro_z
internal: true
temperature:
name: "Température MPU-6050"
id: mpu_temp
internal: true
# 👉 Z en temps réel publiée dans HA
- platform: template
name: "Chatière Accel Z"
id: accel_z_monitor
unit_of_measurement: "m/s²"
accuracy_decimals: 1
update_interval: 0.5s
entity_category: diagnostic
lambda: |-
return id(accel_z).state;
# 👉 Dernière valeur Z en position fermée
- platform: template
name: "Chatière Z Fermée"
id: z_fermee_sensor
unit_of_measurement: "m/s²"
accuracy_decimals: 2
entity_category: diagnostic
lambda: |-
return id(last_closed_z);
# 👉 Dernier pic de mouvement (entrée/sortie)
- platform: template
name: "Chatière Z Pic Mouvement"
id: z_pic_mouvement_sensor
unit_of_measurement: "m/s²"
accuracy_decimals: 2
entity_category: diagnostic
lambda: |-
return id(last_peak_z);
# Capteurs texte
text_sensor:
# Capteur RFID (comme avant)
- platform: template
name: "RFID Chat"
id: rfid_chat
icon: "mdi:cat"
# Nouveau capteur pour le sens de la chatière
- platform: template
name: "Sens Chatière"
id: sens_chatiere
icon: "mdi:door"
# Scripts
script:
- id: clear_chat
then:
- delay: 2s
- text_sensor.template.publish:
id: rfid_chat
state: ""
- id: detect_direction
mode: restart
then:
- lambda: |-
static uint32_t last_change_ms = 0;
static std::string current_state = "Fermée";
static std::string last_published = "";
static uint32_t state_locked_until = 0;
float az = id(accel_z).state;
uint32_t now = millis();
// SEUILS CORRIGÉS pour éviter fausses détections
const float THRESHOLD_SORTIE = -6.5; // Sortie détectée si Z < -6.5
const float THRESHOLD_ENTREE = 6.5; // Entrée détectée si Z > 6.5
const float THRESHOLD_FERME_MIN = 0.8;
const float THRESHOLD_FERME_MAX = 3.0;
const uint32_t DEBOUNCE_MS = 400;
const uint32_t STATE_LOCK_MS = 3000;
bool state_changed = false;
std::string new_state = current_state;
if (now < state_locked_until) {
// Pendant le verrouillage, on continue quand même à suivre le pic
if (current_state == "Entrée" || current_state == "Sortie") {
if (fabsf(az) > fabsf(id(last_peak_z))) {
id(last_peak_z) = az;
}
}
return;
}
// Suivi du pic pendant mouvement (Entrée/Sortie)
if (current_state == "Entrée" || current_state == "Sortie") {
if (fabsf(az) > fabsf(id(last_peak_z))) {
id(last_peak_z) = az;
}
}
// Détection SORTIE (Z < -3.5)
if (current_state == "Fermée" && az < THRESHOLD_SORTIE &&
(now - last_change_ms) > DEBOUNCE_MS) {
new_state = "Sortie";
state_changed = true;
id(last_peak_z) = az; // on initialise le pic à la 1ère valeur
ESP_LOGI("chatiere", "SORTIE (Z=%.1f)", az);
}
// Détection ENTRÉE (Z > 6.5)
else if (current_state == "Fermée" && az > THRESHOLD_ENTREE &&
(now - last_change_ms) > DEBOUNCE_MS) {
new_state = "Entrée";
state_changed = true;
id(last_peak_z) = az; // on initialise le pic à la 1ère valeur
ESP_LOGI("chatiere", "ENTRÉE (Z=%.1f)", az);
}
// Retour FERMÉE (Z entre 1.5 et 6.0)
else if ((current_state == "Entrée" || current_state == "Sortie") &&
az > THRESHOLD_FERME_MIN && az < THRESHOLD_FERME_MAX &&
(now - last_change_ms) > DEBOUNCE_MS) {
new_state = "Fermée";
state_changed = true;
// On mémorise la valeur fermée et on log les infos de calibration
id(last_closed_z) = az;
ESP_LOGI("chatiere", "✓ FERMÉE (Z=%.1f) | Dernier pic mouvement = %.1f",
az, id(last_peak_z));
// Si tu veux, tu peux aussi remettre last_peak_z à 0 après fermeture
// id(last_peak_z) = 0.0f;
}
if (state_changed && new_state != last_published) {
current_state = new_state;
last_published = new_state;
id(sens_chatiere).publish_state(new_state.c_str());
last_change_ms = now;
state_locked_until = now + STATE_LOCK_MS;
ESP_LOGI("chatiere", "✓ Publié: %s | Verrouillé pour 3s", new_state.c_str());
}
# Intervalles
interval:
# Détection RFID
- interval: 100ms
then:
- lambda: |-
static std::string buf;
static uint32_t last_rx_ms = 0;
const uint32_t COOLDOWN_MS = 60000; // 1 minute evite double detection rfid
while (id(rfid_uart).available()) {
uint8_t b;
id(rfid_uart).read_byte(&b);
buf.push_back((char)b);
last_rx_ms = millis();
}
if (!buf.empty() && (millis() - last_rx_ms) > 120) {
auto stx = buf.find('\x02');
auto etx = buf.rfind('\x03');
if (stx != std::string::npos && etx != std::string::npos && etx > stx + 1) {
std::string payload = buf.substr(stx + 1, etx - (stx + 1));
auto tilde = payload.find('\x7E');
if (tilde != std::string::npos) payload = payload.substr(0, tilde);
if (payload.size() >= 16) {
std::string uid = payload.substr(0, 16);
std::string name;
if (uid == "AAAAAAAAAAAAAAAA") name = "Kiwi";
else if (uid == "88ACBA19E3AF0001") name = "Maya";
else name = "Inconnu (" + uid + ")";
struct Seen { std::string u; uint32_t t; };
static Seen last_seen = {"", 0};
const uint32_t now = millis();
const bool is_new_uid = (uid != last_seen.u);
const bool cooldown_over = (now - last_seen.t) > COOLDOWN_MS;
if (is_new_uid || cooldown_over) {
id(rfid_chat).publish_state(name.c_str());
id(clear_chat).execute();
last_seen = {uid, now};
ESP_LOGI("rfid", "Publish: %s", name.c_str());
} else {
ESP_LOGD("rfid", "Duplicate ignored: %s", name.c_str());
}
}
}
buf.clear();
}
# Détection direction chatière
- interval: 100ms
then:
- script.execute: detect_directionLe script évite les doublons, notamment quand les chats rentrent en défonçant la chatière et qu'elle fait plusieurs aller-retours. Il y a aussi un verrouillage temporaire après détection et un temps de refroidissement qui empêche toute répétition immédiate, sur la lecture de la puce, comme sur l'accéléromètre.
On se retrouve avec un nouvel appareil dans Home Assistant :

Il nous reste maintenant à nous occuper de l'accéléromètre, pour savoir si la chatière s'ouvre ou se ferme. Home assistant renvoie les valeurs Z de l'inclinaison. Laissez la chatière fermée, notez la valeur "Chatière Accel Z". Mettez-la en position sortie, notez la nouvelle valeur maximale ; puis notez la valeur maximale en position entrée.
Ensuite, on édite les lignes 200 à 203 du script précédent. Chez moi la chatière fermée a des valeurs comprises entre 1.3 et 2.7 ; cette valeur a tendance à fluctuer avec le temps. J'ai donc noté min 0.8 et max 3.0 pour laisser un peu de marge. Je valide la sortie pour une valeur inférieure à -6.5 (la valeur descend chez moi jusque -11) et l'entrée à partir de 6.5 (la valeur monte chez moi jusque 11).
const float THRESHOLD_SORTIE = -6.5; // Sortie détectée si Z < -6.5
const float THRESHOLD_ENTREE = 6.5; // Entrée détectée si Z > 6.5
const float THRESHOLD_FERME_MIN = 0.8;
const float THRESHOLD_FERME_MAX = 3.0;
Intégration dans Home Assistant
On en vient enfin à ce qui nous intéresse. On va exploiter deux entités créées par notre script ESP 32 : sensor.lecteur_chatiere_rfid_chat et sensor.lecteur_chatiere_sens_chatiere. Je veux recevoir une notification telegram à l'entrée ou à la sortie des chats, et je veux qu'une entité home/not_home soit mise à jour selon la présence des animaux dans la maison.
On commence par créer deux interrupteurs dans Appareils & services > Entrées

Puis on créé des capteurs de présence dans configuration.yaml
- template:
- binary_sensor:
- name: "Kiwi présent"
unique_id: kiwi_present
device_class: presence
state: "{{ is_state('input_boolean.kiwi_home', 'on') }}"
- name: "Maya présente"
unique_id: maya_presente
device_class: presence
state: "{{ is_state('input_boolean.maya_home', 'on') }}"
Et voici l'automatisation finale :
alias: Chatière — RFID + Sens
description: Détecte RFID + direction, met à jour sensor et notifie Telegram
triggers:
- id: rfid_kiwi
trigger: state
entity_id: sensor.lecteur_chatiere_rfid_chat
to: Kiwi
- id: rfid_maya
trigger: state
entity_id: sensor.lecteur_chatiere_rfid_chat
to: Maya
- id: sens_entree
trigger: state
entity_id: sensor.lecteur_chatiere_sens_chatiere
to: Entrée
- id: sens_sortie
trigger: state
entity_id: sensor.lecteur_chatiere_sens_chatiere
to: Sortie
conditions:
- condition: template
value_template: >
{% set last = this.attributes.last_triggered %} {% set last_ts =
as_timestamp(last) if last is not none else 0 %} {{ last is none or
(as_timestamp(now()) - last_ts) > 5 }}
actions:
- variables:
rfid_detected: |-
{% if trigger.id in ['rfid_kiwi','rfid_maya'] %}
{{ trigger.to_state.state }}
{% else %}
{{ states('sensor.lecteur_chatiere_rfid_chat') }}
{% endif %}
sens_detected: |-
{% if trigger.id in ['sens_entree','sens_sortie'] %}
{{ trigger.to_state.state }}
{% else %}
{{ states('sensor.lecteur_chatiere_sens_chatiere') }}
{% endif %}
trigger_source: "{{ trigger.id }}"
- choose:
- conditions:
- condition: template
value_template: "{{ trigger_source in ['rfid_kiwi','rfid_maya'] }}"
sequence:
- wait_for_trigger:
- id: wait_entree
trigger: state
entity_id: sensor.lecteur_chatiere_sens_chatiere
to: Entrée
- id: wait_sortie
trigger: state
entity_id: sensor.lecteur_chatiere_sens_chatiere
to: Sortie
timeout: "00:00:08"
continue_on_timeout: true
- variables:
final_sens: |-
{% if wait.trigger %}
{% if wait.trigger.id == 'wait_entree' %}Entrée
{% elif wait.trigger.id == 'wait_sortie' %}Sortie
{% else %}Inconnu{% endif %}
{% else %}Timeout{% endif %}
final_chat: "{{ rfid_detected }}"
- conditions:
- condition: template
value_template: "{{ trigger_source in ['sens_entree','sens_sortie'] }}"
sequence:
- wait_for_trigger:
- id: wait_kiwi
trigger: state
entity_id: sensor.lecteur_chatiere_rfid_chat
to: Kiwi
- id: wait_maya
trigger: state
entity_id: sensor.lecteur_chatiere_rfid_chat
to: Maya
timeout: "00:00:08"
continue_on_timeout: true
- variables:
final_chat: |-
{% if wait.trigger %}
{% if wait.trigger.id == 'wait_kiwi' %}Kiwi
{% elif wait.trigger.id == 'wait_maya' %}Maya
{% else %}Inconnu{% endif %}
{% else %}Timeout{% endif %}
final_sens: "{{ sens_detected }}"
- choose:
- conditions:
- condition: template
value_template: "{{ final_sens == 'Entrée' and final_chat == 'Kiwi' }}"
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.kiwi_home
- action: notify.telegram
data:
message: 🐾 Kiwi est entré (RFID)
- conditions:
- condition: template
value_template: "{{ final_sens == 'Entrée' and final_chat == 'Maya' }}"
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.maya_home
- action: notify.telegram
data:
message: 🐾 Maya est entrée (RFID)
- conditions:
- condition: template
value_template: "{{ final_sens == 'Sortie' and final_chat == 'Kiwi' }}"
sequence:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.kiwi_home
- action: notify.telegram
data:
message: 🐾 Kiwi est sorti (RFID)
- conditions:
- condition: template
value_template: "{{ final_sens == 'Sortie' and final_chat == 'Maya' }}"
sequence:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.maya_home
- action: notify.telegram
data:
message: 🐾 Maya est sortie (RFID)
default:
- action: notify.telegram
data:
message: >-
⚠️ Passage détecté — Chat: {{ final_chat }} | Direction: {{
final_sens }}
mode: parallel
max: 3
Le résultat en image dans mes notifications Telegram :

Avec le nombre d'allers-retours vous comprenez pourquoi il faut une chatière 😸
Affichage dans le dashboard
Je me suis amusé à créer des templates de suivi pour chaque chat :
- template:
# ----- KIWI -----
- platform: history_stats
name: "Kiwi – entrées (aujourd'hui)"
entity_id: binary_sensor.kiwi_present
state: "on"
type: count
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
- platform: history_stats
name: "Kiwi – sorties (aujourd'hui)"
entity_id: binary_sensor.kiwi_present
state: "off"
type: count
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
- platform: history_stats
name: "Kiwi – temps maison (aujourd'hui)"
entity_id: binary_sensor.kiwi_present
state: "on"
type: time
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
- platform: history_stats
name: "Kiwi – temps dehors (aujourd'hui)"
entity_id: binary_sensor.kiwi_present
state: "off"
type: time
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
# ----- MAYA -----
- platform: history_stats
name: "Maya – entrées (aujourd'hui)"
entity_id: binary_sensor.maya_presente
state: "on"
type: count
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
- platform: history_stats
name: "Maya – sorties (aujourd'hui)"
entity_id: binary_sensor.maya_presente
state: "off"
type: count
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
- platform: history_stats
name: "Maya – temps maison (aujourd'hui)"
entity_id: binary_sensor.maya_presente
state: "on"
type: time
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
- platform: history_stats
name: "Maya – temps dehors (aujourd'hui)"
entity_id: binary_sensor.maya_presente
state: "off"
type: time
start: "{{ today_at('00:00') }}"
end: "{{ now() }}"
Je place mes deux photos des chats dans config/www/
www/kiwi.jpg et www/maya.jpg
Et voici le code lovelace pour le dashboard, un peu compliqué :
type: grid
cards:
- type: heading
heading: Les chats
heading_style: title
- type: custom:stack-in-card
mode: horizontal
cards:
- type: vertical-stack
cards:
- type: picture-elements
image: /local/kiwi.jpg
aspect_ratio: 1/5
tap_action: none
card_mod:
style: |
ha-card {
border-radius: 18px 18px 0 0;
overflow: hidden;
}
elements:
- type: conditional
conditions:
- entity: input_boolean.kiwi_home
state: "on"
elements:
- type: icon
icon: mdi:home
style:
top: 20px
right: "-10px"
color: rgba(76,175,80,0.9)
"--mdc-icon-size": 36px
- type: conditional
conditions:
- entity: input_boolean.kiwi_home
state: "off"
elements:
- type: icon
icon: mdi:home-off-outline
style:
top: 20px
right: "-10px"
color: rgba(220,20,60,0.9)
"--mdc-icon-size": 36px
- type: custom:mushroom-template-card
primary: |
{% if is_state('input_boolean.kiwi_home','on') %}
KIWI PRESENT
{% else %}
KIWI ABSENT
{% endif %}
icon: >
{{
iif(is_state('binary_sensor.kiwi_present','on'),'mdi:home','mdi:home-export-outline')
}}
icon_color: >
{{ iif(is_state('binary_sensor.kiwi_present','on'),'green','red')
}}
multiline_secondary: true
secondary: >
{% set s = as_timestamp(now()) -
as_timestamp(states.input_boolean.kiwi_home.last_changed) %}
{% set m = (s // 60) | int %}
{% set h = (s // 3600) | int %}
{% set d = (s // 86400) | int %}
{% if d >= 1 %}
Depuis {{ d }} jour{{ 's' if d > 1 else '' }}
{% elif h >= 1 %}
Depuis {{ h }} heure{{ 's' if h > 1 else '' }}
{% else %}
Depuis {{ m }} minute{{ 's' if m > 1 else '' }}
{% endif %}
tap_action: none
fill_container: true
card_mod:
style:
.: |
ha-card {
text-align: center;
border-radius: 0 0 18px 18px;
margin-top: -8px;
background-color:
{% if is_state('input_boolean.kiwi_home','on') %}
rgba(76,175,80,.1);
{% else %}
rgba(158,158,158,.1);
{% endif %};
}
:host {
--primary-text-color:
{% if is_state('input_boolean.kiwi_home','on') %}
rgb(76,175,80);
{% else %}
rgb(255,0,0);
{% endif %};
font-weight: 600;
}
mushroom-template-card:
$: |
.primary {
font-size: 1.4rem !important;
font-weight: 700 !important;
line-height: 1.3;
}
.secondary {
font-size: 1.0rem !important;
opacity: 0.9;
line-height: 1.4;
}
- type: vertical-stack
cards:
- type: picture-elements
image: /local/maya.jpg
aspect_ratio: 1/5
tap_action: none
card_mod:
style: |
ha-card {
border-radius: 18px 18px 0 0;
overflow: hidden;
}
elements:
- type: conditional
conditions:
- entity: input_boolean.maya_home
state: "on"
elements:
- type: icon
icon: mdi:home
style:
top: 20px
right: "-10px"
color: rgba(76,175,80,0.9)
"--mdc-icon-size": 36px
- type: conditional
conditions:
- entity: input_boolean.maya_home
state: "off"
elements:
- type: icon
icon: mdi:home-off-outline
style:
top: 20px
right: "-10px"
color: rgba(220,20,60,0.9)
"--mdc-icon-size": 36px
- type: custom:mushroom-template-card
primary: |
{% if is_state('input_boolean.maya_home','on') %}
MAYA PRESENTE
{% else %}
MAYA ABSENTE
{% endif %}
multiline_primary: true
secondary: >
{% set s = as_timestamp(now()) -
as_timestamp(states.input_boolean.maya_home.last_changed) %}
{% set m = (s // 60) | int %}
{% set h = (s // 3600) | int %}
{% set d = (s // 86400) | int %}
{% if d >= 1 %}
Depuis {{ d }} jour{{ 's' if d > 1 else '' }}
{% elif h >= 1 %}
Depuis {{ h }} heure{{ 's' if h > 1 else '' }}
{% else %}
Depuis {{ m }} minute{{ 's' if m > 1 else '' }}
{% endif %}
icon: >
{{
iif(is_state('binary_sensor.maya_presente','on'),'mdi:home','mdi:home-export-outline')
}}
icon_color: >
{{ iif(is_state('binary_sensor.maya_presente','on'),'green','red')
}}
tap_action: none
fill_container: true
card_mod:
style:
.: |
ha-card {
text-align: center;
border-radius: 0 0 18px 18px;
margin-top: -8px;
background-color:
{% if is_state('input_boolean.maya_home','on') %}
rgba(76,175,80,.1);
{% else %}
rgba(158,158,158,.1);
{% endif %};
}
:host {
--primary-text-color:
{% if is_state('input_boolean.maya_home','on') %}
rgb(76,175,80);
{% else %}
rgb(255,0,0);
{% endif %};
font-weight: 600;
}
mushroom-template-card:
$: |
.primary {
font-size: 1.4rem !important;
font-weight: 700 !important;
line-height: 1.3;
}
.secondary {
font-size: 1.0rem !important;
opacity: 0.9;
line-height: 1.4;
}
card_mod:
style: |
ha-card {
background: none;
box-shadow: none;
}
- type: custom:stack-in-card
mode: horizontal
cards:
- type: vertical-stack
cards:
- type: custom:mushroom-template-card
primary: Stats du jour
secondary: >
{% set m_in =
(states('sensor.kiwi_temps_maison_aujourd_hui')|float(0)*60)|int
%} {% set m_out =
(states('sensor.kiwi_temps_dehors_aujourd_hui')|float(0)*60)|int
%} {{ (m_in//60) }}h{{ '%02d' % (m_in%60) }} dedans · {{
(m_out//60) }}h{{ '%02d' % (m_out%60) }} dehors
icon: mdi:calendar-clock
icon_color: purple
multiline_secondary: true
- type: grid
columns: 1
square: false
cards:
- type: custom:mushroom-template-card
primary: Entré
secondary: "{{ states('sensor.kiwi_entrees_aujourd_hui')|int }} fois"
icon: mdi:login
icon_color: teal
- type: custom:mushroom-template-card
primary: Sorti
secondary: "{{ states('sensor.kiwi_sorties_aujourd_hui')|int }} fois"
icon: mdi:logout
icon_color: deep-orange
- type: vertical-stack
cards:
- type: custom:mushroom-template-card
primary: Stats du jour
secondary: >
{% set m_in =
(states('sensor.maya_temps_maison_aujourd_hui')|float(0)*60)|int
%} {% set m_out =
(states('sensor.maya_temps_dehors_aujourd_hui')|float(0)*60)|int
%} {{ (m_in//60) }}h{{ '%02d' % (m_in%60) }} dedans · {{
(m_out//60) }}h{{ '%02d' % (m_out%60) }} dehors
icon: mdi:calendar-clock
icon_color: purple
multiline_secondary: true
- type: grid
columns: 1
square: false
cards:
- type: custom:mushroom-template-card
primary: Entrée
secondary: "{{ states('sensor.maya_entrees_aujourd_hui')|int }} fois"
icon: mdi:login
icon_color: teal
- type: custom:mushroom-template-card
primary: Sortie
secondary: "{{ states('sensor.maya_sorties_aujourd_hui')|int }} fois"
icon: mdi:logout
icon_color: deep-orange
- type: vertical-stack
cards: []
column_span: 1
Ce qui donne ce résultat visuel, avec les deux stars de ce post :

Évolutions possibles
Il arrive rarement que la puce FDX-B ne soit pas détectée, notamment quand les chats passent la chatière en courant. J'ai placé une caméra près de la chatière et dans ce cas, c'est l'IA qui analyse le chat qui passe (via llmvision). Je ne partage pas le code ici, car cela devient très spécifique à mon usage, mais sachez que cela et possible et complète bien la détection.
On pourrait aussi envisager une chatière qui ne s’ouvre que si la puce d’un de mes chats est lue, mais je n’ai personnellement pas cette utilité. Mon chat est le caïd du coin, personne ne vient chez lui (le tigré avec les yeux sournois).