O controle de iluminação inteligente via MQTT permite que qualquer app, dashboard ou automação dimerize uma luz, chame uma cena ou execute uma agenda simplesmente publicando uma mensagem. Neste tutorial você vai construir um dimmer PWM com ESP32 para fitas de LED com fades suaves, projetar topics que funcionem para um cômodo ou para um edifício inteiro, armazenar cenas como mensagens retidas e rodar agendas a partir de um pequeno serviço Python conectado a um broker MQTT gerenciado ConnectMQ.
Escolhendo o Hardware de Iluminação para MQTT
Antes de escrever o firmware, decida que tipo de iluminação você vai controlar. Cada opção se encaixa no MQTT de um jeito um pouco diferente.
| Hardware | Como se conecta ao MQTT | Bom para | Cuidados |
|---|---|---|---|
| Fitas de LED de baixa tensão com driver PWM no ESP32 | Firmware próprio, como neste tutorial | Mesas, prateleiras, armários, iluminação de destaque | Dimensionamento da fonte e queda de tensão em trechos longos |
| LEDs endereçáveis (WS2812B e similares) | Firmware próprio ou WLED | Efeitos e cor por pixel | Consumo de corrente, comprimento da linha de dados |
| Dimmers e relés de rede elétrica com firmware MQTT | Tasmota, Shelly ou dispositivos similares | Luminárias de teto existentes | Apenas hardware certificado, tipo de dimmer correto para a lâmpada |
| Iluminação DALI ou KNX | Bridge do barramento para o MQTT | Escritórios e edifícios comerciais | O barramento continua sendo o controlador; o MQTT acrescenta visibilidade |
Em residências, fitas PWM e relés inteligentes cobrem a maior parte das necessidades. Em edifícios comerciais, a iluminação normalmente já está em DALI ou KNX, e o caminho certo é fazer a bridge desse barramento para o MQTT em vez de substituí-lo. A estrutura de topics abaixo funciona para os quatro casos, e esse é o principal motivo para padronizá-la desde cedo: em um edifício corporativo em São Paulo, os mesmos topics de estado também podem alimentar um gêmeo digital do prédio.
Design de Topics para Luzes
Uma luz precisa de três coisas no MQTT: um topic de comando, um topic de estado e um topic de disponibilidade. Mantê-los separados evita loops de realimentação e permite que os dashboards mostrem o estado real.
<prefix>/<room>/<light>/set commands in (JSON)
<prefix>/<room>/<light>/state actual state out (retained)
<prefix>/<room>/<light>/availability "online" / "offline" (retained, last will)
<prefix>/scenes/<scene> scene definitions (retained)
<prefix>/<room>/scene/set recall a scene by name
Os comandos usam JSON para que você possa estendê-los depois sem quebrar clientes mais antigos:
{"state": "ON", "brightness": 180, "transition": 2.0}
O brightness vai de 0 a 255 e o transition é o tempo de fade em segundos. Esse formato corresponde ao schema JSON que a integração MQTT do Home Assistant entende para luzes, então o mesmo dimmer pode aparecer no Home Assistant sem alterações no firmware.
Hardware
- Uma placa de desenvolvimento ESP32.
- Uma fita de LED monocromática de 12 V ou 24 V e uma fonte compatível.
- Um MOSFET de canal N com nível lógico (por exemplo, IRLZ44N ou AO3400 em um módulo) por canal, ou uma placa driver de MOSFET pronta.
- Um resistor de gate de 220 ohms e um pull-down de 10 kOhms por canal.
Ligue o gate do MOSFET ao GPIO 25 através do resistor, o source ao terra (compartilhado com o terra do ESP32) e o drain ao terminal negativo da fita de LED. O terminal positivo da fita vai ao positivo da fonte. Nunca alimente uma fita pelo pino de 5 V do ESP32.
Para dimmers de rede elétrica, use um produto certificado que já fale MQTT, como um dispositivo Tasmota ou Shelly, em vez de construir você mesmo hardware de tensão de rede. O design de topics deste artigo continua valendo; veja MQTT no Tasmota para gravação e configuração.
O Firmware do Dimmer no ESP32
O sketch usa a API LEDC do core Arduino 3.x para ESP32 (ledcAttach e ledcWrite), PubSubClient e ArduinoJson 7. Ele aplica uma curva gama para que a dimerização pareça linear ao olho, faz fades suaves e publica um estado retido após cada mudança.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <math.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER = "broker.connectmq.com";
const int PORT = 1883;
#define PREFIX "connectmq/demo/light-8pz3/office-201/desk-strip"
const char* T_SET = PREFIX "/set";
const char* T_STATE = PREFIX "/state";
const char* T_AVAIL = PREFIX "/availability";
const int LED_PIN = 25;
const int PWM_FREQ = 5000; // Hz, acima da cintilação visível
const int PWM_BITS = 12; // 0..4095
const int PWM_MAX = (1 << PWM_BITS) - 1;
WiFiClient net;
PubSubClient mqtt(net);
bool on = false;
int brightness = 255; // último brilho 1..255
float current = 0, target = 0; // 0..255
float step = 0;
unsigned long lastStep = 0;
void writePwm(float level) {
float norm = level / 255.0f;
int duty = (int)(powf(norm, 2.2f) * PWM_MAX + 0.5f); // correção gama
ledcWrite(LED_PIN, duty);
}
void publishState() {
JsonDocument doc;
doc["state"] = on ? "ON" : "OFF";
doc["brightness"] = brightness;
char buf[96];
size_t n = serializeJson(doc, buf);
mqtt.publish(T_STATE, (const uint8_t*)buf, n, true);
}
void startFade(float to, float seconds) {
target = to;
int steps = max(1, (int)(seconds * 50)); // 50 atualizações por segundo
step = (target - current) / steps;
if (seconds <= 0) { current = target; writePwm(current); step = 0; }
}
void onMessage(char* topic, byte* payload, unsigned int len) {
JsonDocument doc;
if (deserializeJson(doc, payload, len)) return;
float transition = doc["transition"] | 0.5f;
if (doc["brightness"].is<int>()) {
brightness = constrain(doc["brightness"].as<int>(), 1, 255);
on = true;
}
if (doc["state"].is<const char*>()) {
on = strcmp(doc["state"].as<const char*>(), "ON") == 0;
}
startFade(on ? brightness : 0, transition);
publishState();
}
void connectMqtt() {
while (!mqtt.connected()) {
String id = "light-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (mqtt.connect(id.c_str(), T_AVAIL, 1, true, "offline")) {
mqtt.publish(T_AVAIL, "online", true);
mqtt.subscribe(T_SET, 1);
publishState();
} else {
delay(2000);
}
}
}
void setup() {
ledcAttach(LED_PIN, PWM_FREQ, PWM_BITS);
writePwm(0);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
mqtt.setServer(BROKER, PORT);
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (step != 0 && millis() - lastStep >= 20) {
lastStep = millis();
current += step;
if ((step > 0 && current >= target) || (step < 0 && current <= target)) {
current = target;
step = 0;
}
writePwm(current);
}
}
Observe que o fade roda no loop() sem delay(), então o dispositivo continua respondendo ao MQTT enquanto dimeriza. O last will marca a luz como offline se ela perder energia ou Wi-Fi.
Teste o Dimmer
# observa tudo desta demonstração
mosquitto_sub -h broker.connectmq.com -p 1883 -i light-view-8pz3 -t "connectmq/demo/light-8pz3/#" -v
# liga em cerca de 70 por cento com um fade de 2 segundos
mosquitto_pub -h broker.connectmq.com -p 1883 -i light-cmd-8pz3 -q 1 \
-t "connectmq/demo/light-8pz3/office-201/desk-strip/set" \
-m '{"state":"ON","brightness":180,"transition":2}'
# desliga com um fade lento
mosquitto_pub -h broker.connectmq.com -p 1883 -i light-cmd-8pz3 -q 1 \
-t "connectmq/demo/light-8pz3/office-201/desk-strip/set" \
-m '{"state":"OFF","transition":5}'
O servidor MQTT público é aberto a todos, então um desconhecido poderia acionar a sua luz de demonstração. Isso é inofensivo em uma mesa, mas é exatamente por isso que a iluminação real precisa rodar em um broker privado com credenciais.
Cenas como Mensagens Retidas
Uma cena é um conjunto nomeado de estados de luz. Armazenar cada cena como mensagem retida no broker significa que todos os controladores, dashboards ou painéis de parede veem as mesmas definições, e você pode editar uma cena publicando uma nova versão.
mosquitto_pub -h broker.connectmq.com -p 1883 -i light-cmd-8pz3 -r \
-t "connectmq/demo/light-8pz3/scenes/focus" \
-m '{"office-201/desk-strip":{"state":"ON","brightness":230},"office-201/ceiling":{"state":"ON","brightness":200}}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i light-cmd-8pz3 -r \
-t "connectmq/demo/light-8pz3/scenes/presentation" \
-m '{"office-201/desk-strip":{"state":"ON","brightness":40},"office-201/ceiling":{"state":"OFF"}}'
Chamada de Cenas e Agendas em Python
Um pequeno serviço assina as definições de cenas e os pedidos de chamada, distribui as chamadas de cena para as luzes individuais e executa uma agenda diária. Ele usa apenas paho-mqtt 2.x e a biblioteca padrão.
import json
import time
import uuid
from datetime import datetime
import paho.mqtt.client as mqtt
BROKER, PORT = "broker.connectmq.com", 1883
PREFIX = "connectmq/demo/light-8pz3"
scenes = {}
SCHEDULE = [ # (HH:MM, dias da semana 0=seg..6=dom, cena)
("08:00", range(0, 5), "focus"),
("12:30", range(0, 5), "presentation"),
("19:00", range(0, 7), "off"),
]
scenes["off"] = {"office-201/desk-strip": {"state": "OFF", "transition": 10}}
def recall(client, name):
scene = scenes.get(name)
if not scene:
print("Unknown scene", name)
return
for light, cmd in scene.items():
cmd = {"transition": 1.5, **cmd}
client.publish(f"{PREFIX}/{light}/set", json.dumps(cmd), qos=1)
print("Recalled", name)
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(f"{PREFIX}/scenes/+", qos=1)
client.subscribe(f"{PREFIX}/+/scene/set", qos=1)
def on_message(client, userdata, msg):
parts = msg.topic.split("/")
if parts[-2] == "scenes":
if msg.payload:
scenes[parts[-1]] = json.loads(msg.payload)
elif parts[-2] == "scene" and parts[-1] == "set":
recall(client, msg.payload.decode().strip())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"light-sched-{uuid.uuid4().hex[:6]}")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT, 60)
client.loop_start()
last_run = None
while True:
now = datetime.now()
key = now.strftime("%Y-%m-%d %H:%M")
if key != last_run:
for hhmm, days, scene in SCHEDULE:
if now.strftime("%H:%M") == hhmm and now.weekday() in days:
recall(client, scene)
last_run = key
time.sleep(5)
Chame uma cena manualmente com:
mosquitto_pub -h broker.connectmq.com -p 1883 -i light-cmd-8pz3 \
-t "connectmq/demo/light-8pz3/office-201/scene/set" -m "focus"
Se você prefere um editor visual para agendas e gatilhos baseados no sol, os mesmos topics funcionam no Node-RED ou nas automações do Home Assistant.
Estendendo para Branco Ajustável e RGB
A mesma estrutura de firmware escala para fitas multicanal. Uma fita de branco ajustável tem um canal quente e um frio; uma fita RGB ou RGBW tem três ou quatro. Associe um canal LEDC a cada pino de cor e estenda o comando JSON com campos opcionais, por exemplo "color_temp": 300 em mireds para branco ajustável, ou "color": {"r": 255, "g": 140, "b": 0} para RGB. Mantenha o brightness como campo separado e escale todos os canais por ele, para que brilho e cor possam mudar de forma independente e uma cena possa definir os dois em uma única mensagem.
Dois pontos práticos evitam problemas depois. Faça a mistura do branco ajustável mantendo constante a soma dos dois canais para um dado brilho, senão a luz fica visivelmente mais forte no meio da faixa. E publique o estado resultante completo, incluindo a cor, no topic de estado após cada comando, porque os dashboards e o Home Assistant dependem dele para posicionar corretamente o seletor de cor.
Checklist de Produção
- Migre para um broker privado na porta 8883 com TLS e dê a cada dimmer suas próprias credenciais.
- Mantenha os interruptores de parede funcionando localmente; o MQTT deve acrescentar controle, não ser a única forma de acender uma luz.
- Combine a iluminação com sensores de ocupação de escritório para que salas vazias se apaguem automaticamente.
- Em edifícios com KNX ou DALI, deixe o barramento existente fazer o acionamento e conecte-o ao MQTT via bridge, como em nossa integração KNX MQTT.
- Use QoS 1 para comandos e sempre envie valores absolutos, nunca "toggle", para que uma entrega duplicada não cause problemas.
| Armadilha | Sintoma | Correção |
|---|---|---|
| Sem correção gama | A maior parte da mudança visível acontece no início da faixa | Aplique uma curva de potência como no sketch |
Fade com delay() |
A luz ignora comandos durante os fades | Avance o fade em etapas no loop() |
| Estado não retido | Dashboards mostram "desconhecido" após reconectar | Publique o estado com a flag retain |
| Comandos no topic de estado | As luzes oscilam ou ecoam | Separe os topics set e state |
| Frequência PWM baixa | Cintilação visível na câmera | Use 1 kHz ou mais |
Um broker MQTT gerenciado mantém o controle da iluminação disponível sem um servidor no edifício, e o guia de automação IoT mostra como a iluminação se encaixa com outros sistemas prediais.
Rode Sua Iluminação no ConnectMQ
Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito. Grave seus dimmers com as credenciais emitidas e depois use o cliente MQTT no navegador do portal do ConnectMQ para enviar comandos de teste e chamar cenas enquanto comissiona cada ambiente.
Perguntas frequentes
Por que usar comandos JSON em vez de simples ON e OFF?
O JSON permite que uma única mensagem carregue estado, brilho e tempo de transição, e você pode adicionar campos como temperatura de cor depois sem quebrar os clientes existentes.
Os topics de estado das luzes devem ser retidos?
Sim. Um estado retido permite que qualquer dashboard ou app mostre o estado correto no momento em que assina, sem esperar pela próxima mudança.
Posso controlar luzes de rede elétrica com um ESP32?
Use um dimmer ou módulo de relé de rede elétrica certificado que suporte MQTT, como um dispositivo Tasmota ou Shelly. Construir seu próprio hardware de tensão de rede é arriscado e muitas vezes não atende às normas.
O que acontece com as cenas agendadas se o agendador cair?
As luzes mantêm o estado atual e continuam controláveis pelos interruptores e por comandos MQTT diretos. Rode o agendador como serviço com reinício automático para que eventos perdidos sejam raros.



