Uma boa estrutura de topics MQTT para casa inteligente é previsível e sem surpresas: um ramo por cômodo, um por dispositivo e um conjunto fixo de topics finais para estado, comandos e disponibilidade. Este artigo propõe uma estrutura concreta que você pode copiar, explica por que comandos e estado ficam em topics separados, quais mensagens reter e como a estrutura se encaixa no discovery do Home Assistant e nas ACLs do 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 retainedRegras de nomenclatura
Os nomes dos topics fazem parte da API da sua casa. Mudá-los depois significa mexer em todas as automações, então defina as regras uma vez:
- ASCII minúsculo, palavras unidas por
-:living-room,ceiling-light. Sem espaços, sem acentos, sem emoji. - Sem barra inicial.
/home/kitchencria um primeiro nível vazio e não casa comhome/#. - Nunca comece com
$: esse prefixo é reservado para topics do broker como$SYS. - Substantivos para lugares e coisas, palavras fixas para as folhas:
state,set,availability,event. Não misturestatus,cmdecommandentre dispositivos. - Mantenha a profundidade constante. Todo dispositivo fica exatamente em
home/<room>/<device>, entãohome/+/+/statesempre significa "o estado de todos os dispositivos".
Orientações mais gerais estão em boas práticas de design de topics MQTT.
Por cômodo ou por ID?
| Estilo | Exemplo | Bom para | Contrapartida |
|---|---|---|---|
| Por cômodo | home/kitchen/ceiling-light/state | Casas típicas, automações escritas à mão | Mover um dispositivo muda seus topics |
| Por ID | home/device/shelly-a8032a/state | Instalações grandes ou que mudam com frequência | O cômodo precisa ficar em outro lugar (metadados, áreas do HA) |
| Híbrido | home/kitchen/shelly-a8032a/state | Nomes únicos com agrupamento legível | Ainda muda quando o dispositivo é movido |
Na maioria das casas, a estrutura por cômodo vence pela legibilidade. Se você tem dispositivos que mudam de lugar (um aquecedor portátil, um sensor em um carrinho), dê a eles um ID estável e coloque-os em um ramo neutro como home/mobile/.
Comandos vs. estado
A regra mais importante: o dispositivo publica o estado, todos os outros publicam comandos, e eles nunca compartilham um 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 UIPor que separá-los?
- O topic de estado sempre reflete a realidade. Se o comando falhar (relé travado, dispositivo offline), o topic de estado continua mostrando a verdade e a interface não mente.
- Sem loops de eco. Um dispositivo inscrito no próprio topic de estado receberia as próprias atualizações. Com
setestateseparados, cada lado assina apenas o que precisa. - ACLs seguras. Um usuário de dashboard pode ter permissão para escrever em
.../sete ler.../statesem conseguir falsificar o estado do dispositivo.
O dispositivo deve publicar o novo estado depois de aplicar o comando, mesmo que nada tenha mudado. Essa confirmação é o que interfaces e automações aguardam. Para padrões de requisição/resposta com IDs de correlação, veja comando e controle via MQTT.
O que reter
| Topic | Reter? | Por quê |
|---|---|---|
.../state | Sim | Um dashboard aberto depois mostra o estado atual na hora |
.../availability | Sim | Novos assinantes sabem quais dispositivos estão offline |
.../set | Não | Um comando retido é reproduzido a cada reconexão: acionamentos fantasmas após queda de energia |
.../event | Não | "Botão pressionado" só faz sentido no momento em que acontece |
homeassistant/.../config | Sim | As configurações de discovery precisam sobreviver a reinícios do Home Assistant |
Se um comando foi retido por engano, limpe-o publicando uma mensagem retida vazia no mesmo topic. Detalhes em mensagens retidas.
Disponibilidade com Last Will
Cada dispositivo define um Last Will no seu topic availability ao conectar: payload offline, retido, QoS 1. Logo após conectar, ele publica online, retido. Se o dispositivo perder energia ou Wi-Fi, o broker publica offline quando o keep alive expira, cerca de 1,5 × o intervalo de keep alive. Veja MQTT Last Will.
Um dispositivo em 40 linhas
Este script Python (paho-mqtt 2.x) se comporta como uma lâmpada dimerizável que segue a estrutura. Os exemplos usam o prefixo testmqtt/ para poderem rodar com segurança em um endpoint de testes compartilhado; na sua casa, você o remove.
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 o estado em on_connect é importante: após cada reconexão o estado retido é atualizado e as assinaturas são recriadas mesmo que o broker não tenha mantido sessão.
Teste a estrutura
Acompanhe o estado e a disponibilidade de todos os dispositivos em um terminal:
mosquitto_sub -h broker.connectmq.com -p 1883 -v \
-t 'testmqtt/home/+/+/state' -t 'testmqtt/home/+/+/availability'Envie comandos de outro:
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'Em seguida, confira os comportamentos que a estrutura promete:
- Cada comando gera exatamente uma mensagem de estado.
- Reinicie o terminal 1: o estado atual e
onlineaparecem imediatamente (retidos). - Encerre o script com
kill -9: após o timeout do keep alive, apareceoffline. - Reinicie o script: nenhum comando é reproduzido, porque
setnunca foi retido.
Você pode fazer o mesmo no navegador com o cliente MQTT online e conferir qualquer padrão de assinatura contra seus topics com o topic matcher.
Assinaturas úteis
| Necessidade | Assinatura |
|---|---|
| Tudo da casa | home/# |
| Tudo da cozinha | home/kitchen/# |
| Estado de todos os dispositivos | home/+/+/state |
| Quais dispositivos estão offline | home/+/+/availability |
| Todos os toques em botões | home/+/+/event |
Como a profundidade é constante, curingas de um nível + bastam e você raramente precisa de # fora da depuração. Veja curingas MQTT em profundidade.
Discovery do Home Assistant
O Home Assistant pode criar entidades automaticamente a partir de mensagens de configuração retidas em homeassistant/<component>/<object_id>/config. A estrutura se encaixa diretamente nos 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 e payload_not_available têm como padrão online e offline, exatamente o que o Last Will acima publica. Para a configuração completa da integração, veja Home Assistant e MQTT.
ACLs que seguem a estrutura
Uma árvore consistente deixa as permissões curtas:
| Cliente | Escrita | Leitura |
|---|---|---|
| Cada dispositivo | seus próprios state, availability, event | seu próprio set |
| Home Assistant / hub de automação | home/+/+/set, homeassistant/# | home/# |
| Tablet de parede com dashboard | home/+/+/set | home/+/+/state, home/+/+/availability |
| Usuário convidado ou de monitoramento | nada | home/+/+/availability |
Uma credencial por dispositivo significa que uma tomada inteligente comprometida só pode mentir sobre si mesma, e não destrancar a fechadura da porta de entrada.
Adotar essa estrutura não custa nada e compensa a cada novo dispositivo: profundidade constante, state e set separados, estado e disponibilidade retidos, comandos nunca retidos. Documente as regras, teste cada novo dispositivo com as quatro verificações acima e suas automações continuarão funcionando conforme a casa cresce.
Perguntas frequentes
Os topics de comando MQTT devem ser retidos?
Não. Um comando retido é reenviado ao dispositivo toda vez que ele reconecta, então uma lâmpada pode acender sozinha depois de uma queda de energia. Retenha o estado e a disponibilidade; envie comandos sem a flag retain.
O cômodo deve fazer parte do topic?
Em uma casa típica, sim: home/kitchen/ceiling-light é fácil de ler e permite assinar um cômodo inteiro com home/kitchen/#. A desvantagem é que mover um dispositivo muda seus topics. Instalações maiores costumam usar IDs de dispositivo estáveis e guardar o cômodo como metadado.
Um topic de estado JSON ou um topic por propriedade?
Uma única mensagem de estado JSON por dispositivo é mais fácil de manter consistente e é o que os templates JSON do Home Assistant esperam. Topics por propriedade são mais simples para sensores muito pequenos e algumas ferramentas. Escolha um estilo e use-o em todos os dispositivos.


