Automatización IoT

Control de iluminación inteligente con MQTT: reguladores, tiras LED, escenas y horarios

Controla tiras LED y reguladores por MQTT con un ESP32, usa órdenes JSON con transiciones suaves, guarda las escenas como mensajes retenidos y ejecuta horarios desde un pequeño servicio en Python.

Tira LED regulable bajo un escritorio controlada por un ESP32 en un proyecto de iluminación inteligente con MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

El control de iluminación inteligente con MQTT permite que cualquier app, panel o automatización regule una luz, recupere una escena o ejecute un horario con solo publicar un mensaje. En este tutorial construirás un regulador PWM con ESP32 para tiras LED con fundidos suaves, diseñarás topics que funcionen para una sala o para todo un edificio, guardarás las escenas como mensajes retenidos y ejecutarás horarios desde un pequeño servicio en Python conectado a un broker de ConnectMQ.

Elegir el hardware de iluminación para MQTT

Antes de escribir el firmware, decide qué tipo de iluminación vas a controlar. Cada opción encaja con MQTT de una forma ligeramente distinta.

Hardware Cómo se conecta a MQTT Ideal para Precauciones
Tiras LED de baja tensión con un driver PWM en ESP32 Firmware propio, como en este tutorial Escritorios, estanterías, armarios, iluminación decorativa Dimensionado de la fuente de alimentación y caída de tensión en tramos largos
LED direccionables (WS2812B y similares) Firmware propio o WLED Efectos y color por píxel Consumo de corriente, longitud de la línea de datos
Reguladores y relés de red con firmware MQTT Tasmota, Shelly o dispositivos similares Luces de techo existentes Solo hardware certificado, tipo de regulador correcto para la lámpara
Iluminación DALI o KNX Puente desde el bus hacia MQTT Oficinas y edificios comerciales El bus sigue siendo el controlador; MQTT añade visibilidad

En los hogares, las tiras PWM y los relés inteligentes cubren la mayoría de las necesidades. En los edificios comerciales, la iluminación suele estar ya en DALI o KNX, y lo correcto es puentear ese bus a MQTT en lugar de sustituirlo. La estructura de topics que sigue funciona para las cuatro opciones, que es la principal razón para estandarizarla pronto.

Diseño de topics para luces

Una luz necesita tres cosas en MQTT: un topic de órdenes, un topic de estado y un topic de disponibilidad. Mantenerlos separados evita bucles de realimentación y permite que los paneles muestren el 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

Las órdenes usan JSON para que puedas ampliarlas más adelante sin romper los clientes antiguos:

{"state": "ON", "brightness": 180, "transition": 2.0}

brightness va de 0 a 255 y transition es el tiempo de fundido en segundos. Este formato coincide con el esquema JSON que entiende la integración MQTT de Home Assistant para las luces, así que el mismo regulador puede aparecer en Home Assistant sin cambios en el firmware.

Hardware

  • Una placa de desarrollo ESP32.
  • Una tira LED monocolor de 12 V o 24 V y una fuente de alimentación adecuada.
  • Un MOSFET de canal N de nivel lógico (por ejemplo IRLZ44N, o AO3400 en un módulo) por canal, o una placa driver de MOSFET ya montada.
  • Una resistencia de puerta de 220 ohmios y una pull-down de 10 kOhm por canal.

Conecta la puerta del MOSFET al GPIO 25 a través de la resistencia, la fuente a masa (compartida con la masa del ESP32) y el drenador al cable negativo de la tira LED. El cable positivo de la tira va al positivo de la fuente. Nunca alimentes una tira desde el pin de 5 V del ESP32.

Para reguladores de red, usa un producto certificado que ya hable MQTT, como un dispositivo Tasmota o Shelly, en lugar de construir tú mismo hardware de tensión de red. El diseño de topics de este artículo sigue siendo aplicable; consulta Tasmota con MQTT para la grabación y la configuración.

El firmware del regulador ESP32

El sketch usa la API LEDC del core Arduino para ESP32 3.x (ledcAttach y ledcWrite), PubSubClient y ArduinoJson 7. Aplica una curva gamma para que la regulación parezca lineal al ojo, hace fundidos suaves y publica un estado retenido tras cada cambio.

#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, por encima del parpadeo visible
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 brillo 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);   // corrección gamma
  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 actualizaciones 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);
  }
}

Observa que el fundido se ejecuta en loop() sin delay(), de modo que el dispositivo sigue respondiendo a MQTT mientras regula. La última voluntad marca la luz como offline si pierde la alimentación o el Wi-Fi.

Prueba el regulador

# observa todo lo de esta demo
mosquitto_sub -h broker.connectmq.com -p 1883 -i light-view-8pz3 -t "connectmq/demo/light-8pz3/#" -v

# enciende a un 70 por ciento aproximadamente con un fundido 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}'

# apaga con un fundido 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}'

El servidor MQTT público gratuito está abierto a todo el mundo y solo sirve para pruebas y prototipos, así que un desconocido podría encender o apagar tu luz de demostración. En un escritorio es inofensivo, pero es exactamente la razón por la que la iluminación real debe funcionar en un broker privado con credenciales.

Escenas como mensajes retenidos

Una escena es un conjunto de estados de luz con nombre. Guardar cada escena como mensaje retenido en el broker significa que todos los controladores, paneles o pulsadores de pared ven las mismas definiciones, y puedes editar una escena publicando una nueva versión.

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"}}'

Recuperación de escenas y horarios en Python

Un pequeño servicio se suscribe a las definiciones de escena y a las peticiones de recuperación, reparte cada recuperación de escena entre las luces individuales y ejecuta un horario diario. Usa paho-mqtt 2.x y solo la librería estándar.

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, días de la semana 0=lun..6=dom, escena)
    ("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)

Recupera una escena manualmente con:

mosquitto_pub -h broker.connectmq.com -p 1883 -i light-cmd-8pz3 \
  -t "connectmq/demo/light-8pz3/office-201/scene/set" -m "focus"

Si prefieres un editor visual para los horarios y los disparadores basados en el sol, los mismos topics funcionan desde Node-RED o desde las automatizaciones de Home Assistant.

Ampliar a blanco regulable y RGB

La misma estructura de firmware escala a tiras multicanal. Una tira de blanco regulable tiene un canal cálido y otro frío; una tira RGB o RGBW tiene tres o cuatro. Asigna un canal LEDC por cada pin de color y amplía la orden JSON con campos opcionales, por ejemplo "color_temp": 300 en mireds para el blanco regulable, o "color": {"r": 255, "g": 140, "b": 0} para RGB. Mantén brightness como campo independiente y escala cada canal con él, de modo que el brillo y el color puedan cambiar por separado y una escena pueda fijar ambos en un solo mensaje.

Dos detalles prácticos te ahorrarán problemas más adelante. Mezcla el blanco regulable manteniendo constante la suma de los dos canales para un brillo dado; de lo contrario, la luz se vuelve notablemente más brillante en la mitad del rango. Y publica el estado resultante completo, color incluido, en el topic de estado tras cada orden, porque los paneles y Home Assistant dependen de él para situar correctamente el selector de color.

Lista de comprobación para producción

  1. Pasa a un broker privado en el puerto 8883 con TLS y da a cada regulador sus propias credenciales.
  2. Mantén operativos en local los interruptores de pared; MQTT debe añadir control, no ser la única forma de encender una luz.
  3. Combina la iluminación con sensores de ocupación de oficinas para que las salas vacías se apaguen automáticamente.
  4. En edificios con KNX o DALI, deja que el bus existente haga la conmutación y puentéalo a MQTT, como en nuestra integración KNX MQTT.
  5. Usa QoS 1 para las órdenes y envía siempre valores absolutos, nunca "toggle", para que una entrega duplicada no cause daños.
Error Síntoma Solución
Sin corrección gamma La mayor parte del cambio visible ocurre en la parte baja del rango Aplica una curva de potencia como en el sketch
Fundido con delay() La luz ignora las órdenes durante los fundidos Avanza el fundido por pasos en loop()
Estado no retenido Los paneles muestran "desconocido" tras reconectar Publica el estado con el indicador de retención
Órdenes en el topic de estado Las luces oscilan o hacen eco Separa los topics set y state
Frecuencia PWM baja Parpadeo visible en cámara Usa 1 kHz o más

Un broker MQTT gestionado mantiene disponible el control de la iluminación sin un servidor en el edificio, y la guía de automatización IoT muestra cómo encaja la iluminación con otros sistemas del edificio. Los mismos topics de estado retenidos pueden alimentar un gemelo digital 3D del edificio en el que cada luminaria muestra su estado real sobre el plano.

Ejecuta tu iluminación en ConnectMQ

Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito. Graba tus reguladores con las credenciales emitidas y después usa el cliente MQTT del navegador integrado en el portal de ConnectMQ para enviar órdenes de prueba y recuperar escenas mientras pones en marcha cada sala.

Preguntas frecuentes

¿Por qué usar órdenes JSON en lugar de ON y OFF sin más?

JSON permite que un solo mensaje lleve el estado, el brillo y el tiempo de transición, y puedes añadir más adelante campos como la temperatura de color sin romper los clientes existentes.

¿Deben retenerse los topics de estado de las luces?

Sí. Un estado retenido permite que cualquier panel o app muestre el estado correcto en cuanto se suscribe, sin esperar al siguiente cambio.

¿Puedo controlar luces de red con un ESP32?

Usa un módulo regulador o de relé de red certificado que admita MQTT, como un dispositivo Tasmota o Shelly. Construir tu propio hardware de tensión de red es arriesgado y a menudo no cumple la normativa.

¿Qué pasa con las escenas programadas si el programador está caído?

Las luces mantienen su estado actual y siguen siendo controlables mediante los interruptores y las órdenes MQTT directas. Ejecuta el programador como un servicio con reinicio automático para que los eventos perdidos sean poco frecuentes.

Artículos relacionados

Guía para configurar Zigbee2MQTT: del coordinador al panel

Instala Zigbee2MQTT con Docker, configura el coordinador y la conexión MQTT, empareja y renombra dispositivos, contrólalos por MQTT y consulta sus estados en un panel, probando primero en un servidor MQTT público gratuito.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.