Tutoriels

Conception des topics pour la maison connectée : une organisation MQTT pratique

Une organisation de topics MQTT pratique pour la maison : pièces et appareils, topics d'état et de commande, retained, disponibilité, Home Assistant et ACL.

Mis à jour · 9 min de lecture

Conception des topics pour la maison connectée : une organisation MQTT pratique

Une bonne organisation des topics MQTT pour la maison connectée est ennuyeuse et prévisible : une branche par pièce, une par appareil et un jeu fixe de topics terminaux pour l'état, les commandes et la disponibilité. Cet article propose une organisation concrète à reprendre telle quelle, explique pourquoi commandes et état vivent sur des topics séparés, quels messages retenir et comment cette organisation se traduit dans le discovery de Home Assistant et les ACL du broker.

L'organisation
home/<room>/<device>/state          current state, JSON, retained
home/<room>/<device>/set            commands to the device, NOT retained
home/<room>/<device>/availability   "online" / "offline", retained, Last Will
home/<room>/<device>/event          one-off events (button press), NOT retained

Règles de nommage

Les noms de topics font partie de l'API de votre maison. Les changer plus tard oblige à toucher chaque automatisation : fixez les règles une fois pour toutes.

  • ASCII en minuscules, mots reliés par - : living-room, ceiling-light. Pas d'espaces, pas d'accents, pas d'emoji.
  • Pas de slash initial. /home/kitchen crée un premier niveau vide et ne correspond pas à home/#.
  • Ne jamais commencer par $ : ce préfixe est réservé aux topics du broker comme $SYS.
  • Des noms pour les lieux et les objets, des mots fixes pour les feuilles : state, set, availability, event. Ne mélangez pas status, cmd et command d'un appareil à l'autre.
  • Gardez une profondeur constante. Chaque appareil se trouve exactement en home/<room>/<device>, donc home/+/+/state signifie toujours « l'état de tous les appareils ».

Des conseils plus généraux sont disponibles dans bonnes pratiques de conception des topics MQTT.

Par pièce ou par identifiant ?

StyleExempleAdapté àContrepartie
Par piècehome/kitchen/ceiling-light/stateMaisons typiques, automatisations écrites à la mainDéplacer un appareil change ses topics
Par identifianthome/device/shelly-a8032a/stateInstallations grandes ou changeantesLa pièce doit être stockée ailleurs (métadonnées, zones HA)
Hybridehome/kitchen/shelly-a8032a/stateNoms uniques avec regroupement lisibleChange quand même lors d'un déplacement

Dans la plupart des maisons, l'organisation par pièce l'emporte pour la lisibilité. Si certains appareils se déplacent (un radiateur d'appoint, un capteur sur un chariot), donnez-leur un identifiant stable et placez-les sous une branche neutre comme home/mobile/.

Commandes et état

La règle la plus importante : l'appareil publie l'état, tous les autres publient des commandes, et ils ne partagent jamais un topic.

Un basculement, de bout en bout
app      ──PUBLISH home/kitchen/ceiling-light/set   {"state":"ON"}            (not retained)
light    ──applies the command, then
light    ──PUBLISH home/kitchen/ceiling-light/state {"state":"ON","brightness":255}  (retained)
app, HA  ◀─ receive the new state and update the UI

Pourquoi les séparer ?

  • Le topic d'état reflète toujours la réalité. Si la commande échoue (relais bloqué, appareil hors ligne), le topic d'état continue d'afficher la vérité et l'interface ne ment pas.
  • Pas de boucles d'écho. Un appareil abonné à son propre topic d'état recevrait ses propres mises à jour. Avec set et state séparés, chaque partie ne s'abonne qu'à ce dont elle a besoin.
  • Des ACL sûres. Un utilisateur de dashboard peut être autorisé à écrire sur .../set et à lire .../state sans pouvoir falsifier l'état de l'appareil.

L'appareil doit publier le nouvel état après avoir appliqué la commande, même si rien n'a changé. C'est cette confirmation qu'attendent les interfaces et les automatisations. Pour les schémas requête/réponse avec identifiants de corrélation, voir commande et contrôle via MQTT.

Que retenir ?

TopicRetenir ?Pourquoi
.../stateOuiUn dashboard ouvert plus tard affiche immédiatement l'état actuel
.../availabilityOuiLes nouveaux abonnés savent quels appareils sont hors ligne
.../setNonUne commande retenue est rejouée à chaque reconnexion : allumages fantômes après une coupure
.../eventNon« Bouton appuyé » n'a de sens qu'au moment où cela se produit
homeassistant/.../configOuiLes configurations de discovery doivent survivre aux redémarrages de Home Assistant

Si une commande a été retenue par erreur, effacez-la en publiant un message retenu vide sur le même topic. Détails dans messages retenus.

Disponibilité avec Last Will

Chaque appareil définit un Last Will sur son topic availability à la connexion : payload offline, retenu, QoS 1. Juste après la connexion, il publie online, retenu. Si l'appareil perd l'alimentation ou le Wi-Fi, le broker publie offline à l'expiration du keep alive, soit environ 1,5 × l'intervalle de keep alive. Voir MQTT Last Will.

Un appareil en 40 lignes

Ce script Python (paho-mqtt 2.x) se comporte comme une lampe à variateur qui respecte l'organisation. Les exemples utilisent le préfixe testmqtt/ pour pouvoir tourner sans risque sur un endpoint de test partagé ; chez vous, supprimez-le.

fake_light.py
import json, os
import paho.mqtt.client as mqtt

BASE = "testmqtt/home/kitchen/ceiling-light"
state = {"state": "OFF", "brightness": 0}

def publish_state(client):
    client.publish(f"{BASE}/state", json.dumps(state), qos=1, retain=True)

def on_connect(client, userdata, flags, reason_code, properties):
    client.publish(f"{BASE}/availability", "online", qos=1, retain=True)
    client.subscribe(f"{BASE}/set", qos=1)
    publish_state(client)

def on_message(client, userdata, msg):
    try:
        cmd = json.loads(msg.payload)
    except ValueError:
        cmd = {"state": msg.payload.decode(errors="replace").strip().upper()}
    if not isinstance(cmd, dict):
        cmd = {}
    if cmd.get("state") in ("ON", "OFF"):
        state["state"] = cmd["state"]
    if isinstance(cmd.get("brightness"), int):
        state["brightness"] = max(0, min(255, cmd["brightness"]))
    publish_state(client)  # always confirm with the real state

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="kitchen-ceiling-light")
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.environ["MQTT_USER"], os.getenv("MQTT_PASS"))
client.will_set(f"{BASE}/availability", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_forever()

Publier l'état dans on_connect est important : après chaque reconnexion, l'état retenu est rafraîchi et les abonnements sont recréés même si le broker n'a conservé aucune session.

Tester l'organisation

Observez l'état et la disponibilité de tous les appareils dans un terminal :

Terminal 1 — observer
mosquitto_sub -h broker.connectmq.com -p 1883 -v \
  -t 'testmqtt/home/+/+/state' -t 'testmqtt/home/+/+/availability'

Envoyez des commandes depuis un autre :

Terminal 2 — commandes
mosquitto_pub -h broker.connectmq.com -p 1883 -q 1 \
  -t 'testmqtt/home/kitchen/ceiling-light/set' -m '{"state":"ON","brightness":180}'

mosquitto_pub -h broker.connectmq.com -p 1883 -q 1 \
  -t 'testmqtt/home/kitchen/ceiling-light/set' -m 'off'

Vérifiez ensuite les comportements que promet l'organisation :

  1. Chaque commande produit exactement un message d'état.
  2. Relancez le terminal 1 : l'état actuel et online apparaissent immédiatement (retenus).
  3. Tuez le script avec kill -9 : après le délai du keep alive, offline apparaît.
  4. Relancez le script : aucune commande n'est rejouée, car set n'a jamais été retenu.

Vous pouvez faire la même chose dans le navigateur avec le client MQTT en ligne, et vérifier n'importe quel motif d'abonnement contre vos topics avec le topic matcher.

Abonnements utiles

BesoinAbonnement
Toute la maisonhome/#
Toute la cuisinehome/kitchen/#
L'état de chaque appareilhome/+/+/state
Les appareils hors lignehome/+/+/availability
Tous les appuis sur des boutonshome/+/+/event

Comme la profondeur est constante, les jokers à un niveau + suffisent et vous avez rarement besoin de # en dehors du débogage. Voir les jokers MQTT en détail.

Discovery de Home Assistant

Home Assistant peut créer des entités automatiquement à partir de messages de configuration retenus sous homeassistant/<component>/<object_id>/config. L'organisation se transpose directement sur les champs de discovery :

homeassistant/switch/kitchen_ceiling_light/config (retenu)
{
  "name": "Kitchen ceiling light",
  "unique_id": "kitchen_ceiling_light",
  "state_topic": "home/kitchen/ceiling-light/state",
  "value_template": "{{ value_json.state }}",
  "command_topic": "home/kitchen/ceiling-light/set",
  "payload_on": "ON",
  "payload_off": "OFF",
  "availability_topic": "home/kitchen/ceiling-light/availability"
}

payload_available et payload_not_available valent par défaut online et offline, ce que publie justement le Last Will ci-dessus. Pour la configuration complète de l'intégration, voir Home Assistant et MQTT.

Des ACL qui suivent l'organisation

Une arborescence cohérente rend les permissions concises :

ClientÉcritureLecture
Chaque appareilses propres state, availability, eventson propre set
Home Assistant / hub d'automatisationhome/+/+/set, homeassistant/#home/#
Tablette murale avec dashboardhome/+/+/sethome/+/+/state, home/+/+/availability
Utilisateur invité ou de supervisionrienhome/+/+/availability

Un identifiant par appareil signifie qu'une prise connectée compromise ne peut mentir que sur elle-même, et non ouvrir la serrure de votre porte d'entrée.

Adopter cette organisation ne coûte rien et rapporte à chaque nouvel appareil : profondeur constante, state et set séparés, état et disponibilité retenus, commandes jamais retenues. Écrivez les règles, testez chaque nouvel appareil avec les quatre vérifications ci-dessus, et vos automatisations continueront de fonctionner à mesure que la maison s'agrandit.

Questions fréquentes

Les topics de commande MQTT doivent-ils être retenus ?

Non. Une commande retenue est renvoyée à l'appareil à chaque reconnexion : une lampe peut alors s'allumer toute seule après une coupure de courant. Retenez l'état et la disponibilité ; envoyez les commandes sans le flag retain.

La pièce doit-elle faire partie du topic ?

Dans une maison typique, oui : home/kitchen/ceiling-light se lit bien et permet de s'abonner à toute une pièce avec home/kitchen/#. L'inconvénient : déplacer un appareil change ses topics. Les grandes installations utilisent souvent des identifiants d'appareil stables et gardent la pièce comme métadonnée.

Un topic d'état JSON ou un topic par propriété ?

Un seul message d'état JSON par appareil est plus facile à garder cohérent et correspond à ce qu'attendent les templates JSON de Home Assistant. Les topics par propriété sont plus simples pour les très petits capteurs et certains outils. Choisissez un style et appliquez-le à tous les appareils.