Una buena estructura de topics MQTT para la casa inteligente es aburrida y predecible: una rama por habitación, una por dispositivo y un conjunto fijo de topics finales para estado, comandos y disponibilidad. Este artículo propone una estructura concreta que puedes copiar, explica por qué comandos y estado van en topics separados, qué mensajes retener y cómo se traslada la estructura al discovery de Home Assistant y a las ACLs del broker.
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 retainedReglas de nombres
Los nombres de los topics forman parte de la API de tu casa. Cambiarlos más tarde implica tocar cada automatización, así que decide las reglas una sola vez:
- ASCII en minúsculas, palabras unidas con
-:living-room,ceiling-light. Sin espacios, sin tildes ni eñes, sin emojis. - Sin barra inicial.
/home/kitchencrea un primer nivel vacío y no coincide conhome/#. - Nunca empieces por
$: ese prefijo está reservado para topics del broker como$SYS. - Sustantivos para lugares y cosas, palabras fijas para las hojas:
state,set,availability,event. No mezclesstatus,cmdycommandentre dispositivos. - Mantén la profundidad constante. Cada dispositivo está exactamente en
home/<room>/<device>, así quehome/+/+/statesiempre significa "el estado de todos los dispositivos".
Encontrarás pautas más generales en buenas prácticas de diseño de topics MQTT.
¿Por habitación o por ID?
| Estilo | Ejemplo | Ideal para | Contrapartida |
|---|---|---|---|
| Por habitación | home/kitchen/ceiling-light/state | Casas típicas, automatizaciones escritas a mano | Mover un dispositivo cambia sus topics |
| Por ID | home/device/shelly-a8032a/state | Instalaciones grandes o que cambian a menudo | La habitación debe guardarse en otro sitio (metadatos, áreas de HA) |
| Híbrido | home/kitchen/shelly-a8032a/state | Nombres únicos con agrupación legible | Sigue cambiando al mover el dispositivo |
En la mayoría de casas gana la estructura por habitación por legibilidad. Si tienes dispositivos que se mueven (un calefactor portátil, un sensor en un carrito), dales un ID estable y ponlos bajo una rama neutra como home/mobile/.
Comandos frente a estado
La regla más importante: el dispositivo publica el estado, todos los demás publican comandos, y nunca comparten topic.
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¿Por qué separarlos?
- El topic de estado siempre refleja la realidad. Si el comando falla (relé atascado, dispositivo desconectado), el topic de estado sigue mostrando la verdad y la interfaz no miente.
- Sin bucles de eco. Un dispositivo suscrito a su propio topic de estado recibiría sus propias actualizaciones. Con
setystateseparados, cada parte se suscribe solo a lo que necesita. - ACLs seguras. A un usuario de dashboard se le puede permitir escribir en
.../sety leer.../statesin que pueda falsificar el estado del dispositivo.
El dispositivo debe publicar el nuevo estado después de aplicar el comando, aunque no haya cambiado nada. Esa confirmación es lo que esperan las interfaces y las automatizaciones. Para patrones de petición/respuesta con IDs de correlación, consulta comando y control sobre MQTT.
Qué retener
| Topic | ¿Retener? | Por qué |
|---|---|---|
.../state | Sí | Un dashboard que se abre más tarde muestra el estado actual al instante |
.../availability | Sí | Los nuevos suscriptores saben qué dispositivos están desconectados |
.../set | No | Un comando retenido se repite en cada reconexión: encendidos fantasma tras un corte de luz |
.../event | No | "Botón pulsado" solo tiene sentido en el momento en que ocurre |
homeassistant/.../config | Sí | Las configuraciones de discovery deben sobrevivir a los reinicios de Home Assistant |
Si se retuvo un comando por error, bórralo publicando un mensaje retenido vacío en el mismo topic. Detalles en mensajes retenidos.
Disponibilidad con Last Will
Cada dispositivo define un Last Will en su topic availability al conectarse: payload offline, retenido, QoS 1. Justo después de conectarse publica online, retenido. Si el dispositivo pierde la alimentación o el Wi-Fi, el broker publica offline cuando vence el keep alive, aproximadamente 1,5 × el intervalo de keep alive. Consulta MQTT Last Will.
Un dispositivo en 40 líneas
Este script de Python (paho-mqtt 2.x) se comporta como una luz regulable que sigue la estructura. Los ejemplos usan el prefijo testmqtt/ para poder ejecutarse sin riesgo en un endpoint de pruebas compartido; en tu casa lo quitas.
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()Publicar el estado en on_connect es importante: tras cada reconexión se refresca el estado retenido y las suscripciones se vuelven a crear aunque el broker no haya guardado sesión.
Prueba la estructura
Observa el estado y la disponibilidad de todos los dispositivos en una terminal:
mosquitto_sub -h broker.connectmq.com -p 1883 -v \
-t 'testmqtt/home/+/+/state' -t 'testmqtt/home/+/+/availability'Envía comandos desde otra:
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'Después comprueba los comportamientos que promete la estructura:
- Cada comando produce exactamente un mensaje de estado.
- Reinicia la terminal 1: el estado actual y
onlineaparecen al instante (retenidos). - Mata el script con
kill -9: tras el timeout de keep alive apareceoffline. - Reinicia el script: no se repite ningún comando, porque
setnunca se retuvo.
Puedes hacer lo mismo en el navegador con el cliente MQTT online y comprobar cualquier patrón de suscripción contra tus topics con el topic matcher.
Suscripciones útiles
| Necesidad | Suscripción |
|---|---|
| Todo lo de la casa | home/# |
| Todo lo de la cocina | home/kitchen/# |
| Estado de todos los dispositivos | home/+/+/state |
| Qué dispositivos están desconectados | home/+/+/availability |
| Todas las pulsaciones de botones | home/+/+/event |
Como la profundidad es constante, bastan los comodines de un nivel + y rara vez necesitas # fuera de la depuración. Consulta comodines MQTT en profundidad.
Discovery de Home Assistant
Home Assistant puede crear entidades automáticamente a partir de mensajes de configuración retenidos bajo homeassistant/<component>/<object_id>/config. La estructura encaja directamente con los campos de discovery:
{
"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 y payload_not_available valen por defecto online y offline, que es justo lo que publica el Last Will anterior. Para la configuración completa de la integración consulta Home Assistant y MQTT.
ACLs que siguen la estructura
Un árbol coherente hace que los permisos sean cortos:
| Cliente | Escritura | Lectura |
|---|---|---|
| Cada dispositivo | su propio state, availability, event | su propio set |
| Home Assistant / hub de automatización | home/+/+/set, homeassistant/# | home/# |
| Tablet de pared con dashboard | home/+/+/set | home/+/+/state, home/+/+/availability |
| Usuario invitado o de monitorización | nada | home/+/+/availability |
Una credencial por dispositivo significa que un enchufe inteligente comprometido solo puede mentir sobre sí mismo, no abrir la cerradura de tu puerta.
Adoptar esta estructura no cuesta nada y compensa con cada nuevo dispositivo: profundidad constante, state y set separados, estado y disponibilidad retenidos, comandos nunca retenidos. Pon las reglas por escrito, prueba cada nuevo dispositivo con las cuatro comprobaciones anteriores y tus automatizaciones seguirán funcionando a medida que crezca la casa.
Preguntas frecuentes
¿Deben ser retenidos los topics de comando MQTT?
No. Un comando retenido se reenvía al dispositivo cada vez que se reconecta, así que una luz puede encenderse sola tras un corte de luz. Retén el estado y la disponibilidad; envía los comandos sin el flag retain.
¿Debe la habitación formar parte del topic?
En una casa típica, sí: home/kitchen/ceiling-light se lee bien y te permite suscribirte a una habitación entera con home/kitchen/#. La desventaja es que mover un dispositivo cambia sus topics. Las instalaciones grandes suelen usar IDs de dispositivo estables y guardar la habitación como metadato.
¿Un topic de estado JSON o un topic por propiedad?
Un único mensaje de estado JSON por dispositivo es más fácil de mantener coherente y es lo que esperan las plantillas JSON de Home Assistant. Los topics por propiedad son más simples para sensores muy pequeños y algunas herramientas. Elige un estilo y úsalo en todos los dispositivos.


