Cloud, DevOps e IoT en español

Parte 18 de 22 de la serie Domótica con ESP32 y AWS desde cero

Bombillo inteligente DIY con ESP32-C3 e interruptor de pared que funciona sin internet

10 de octubre de 2026 · Steven Carvajal · Proyectos

Código de este artículo en GitHub →

Una casa inteligente que solo se controla desde el celular es peor que una casa normal. Si se cae internet, si el celular no tiene batería o si llega una visita, tiene que haber un interruptor en la pared que funcione como siempre.

En esta parte agregamos a un nodo ESP32-C3 un interruptor físico que funciona sin internet y sin el hub, y vemos cómo mantenerlo sincronizado con la app sin que la nube deshaga el cambio.

El nodo

Un ESP32-C3 Super Mini que:

Un interruptor que alterna, no que manda

La primera idea sería leer la posición del interruptor: arriba es encendido, abajo es apagado. No funciona con un sistema que también se controla desde la app:

  1. El interruptor está abajo (apagado).
  2. Alguien enciende la luz desde la app.
  3. Ahora el interruptor dice "apagado" y la luz está encendida.

Por eso el nodo no mira la posición, mira el cambio: cada vez que alguien mueve el interruptor, en cualquier dirección, el estado se invierte. Es lo mismo que hacen muchos relés inteligentes comerciales en modo "alternar". Sin importar en qué posición quedó el interruptor, moverlo siempre cambia la luz.

El cableado del interruptor

El interruptor de pared tiene dos cables. Uno va al GPIO 10 del ESP32-C3 y el otro a GND. No hace falta resistencia: se activa la resistencia pull-up interna del chip.

El GPIO 10 es una buena elección porque en el ESP32-C3 los pines 2, 8 y 9 intervienen en el arranque, y un interruptor conectado a ellos podría impedir que la placa arranque según su posición.

Cara trasera del ESP32-C3 Super Mini con la serigrafía de los pines; el GPIO 10 está a la derecha y G, el pin de tierra, a la izquierda

El interruptor va al GPIO 10 (a la derecha) y a G, tierra (a la izquierda).

Si el cable del interruptor es largo, puede captar ruido. Un condensador pequeño (100 nF) entre el GPIO y GND, cerca de la placa, ayuda.

El código del nodo

#define PIN_INTERRUPTOR 10
#define PIN_SALIDA      8    // LED del C3 Super Mini; en la versión final, el pin del relé
#define ANTIRREBOTE_MS  50

bool encendido = false;
int ultimaLectura = HIGH;
int lecturaEstable = HIGH;
unsigned long cambioEn = 0;

void aplicar(bool on) {
  encendido = on;
  digitalWrite(PIN_SALIDA, on ? LOW : HIGH);  // el LED del C3 Super Mini es activo en bajo
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_SALIDA, OUTPUT);
  aplicar(false);
  pinMode(PIN_INTERRUPTOR, INPUT_PULLUP);
  ultimaLectura = lecturaEstable = digitalRead(PIN_INTERRUPTOR);  // la posición inicial no cuenta
  // ... ESP-NOW como en la parte 4
}

void revisarInterruptor() {
  int lectura = digitalRead(PIN_INTERRUPTOR);
  if (lectura != ultimaLectura) {   // el contacto se movió (o rebota)
    ultimaLectura = lectura;
    cambioEn = millis();
  }
  if (millis() - cambioEn < ANTIRREBOTE_MS || lectura == lecturaEstable) return;

  lecturaEstable = lectura;          // cambio real y estable
  aplicar(!encendido);               // primero la luz, sin esperar a nadie
  Serial.printf("Interruptor: salida %s\n", encendido ? "encendida" : "apagada");
  avisarAlHub(encendido);            // después avisar, si hay alguien escuchando
}

void loop() {
  revisarInterruptor();
}

El orden es lo importante: primero se aplica el cambio localmente y después se avisa. Si el hub está apagado o no hay internet, la luz cambia igual.

El antirrebote evita que un solo movimiento cuente varias veces: un contacto mecánico genera varios cambios muy rápidos al moverse, y solo se acepta una lectura que se mantiene 50 ms.

Cuando llega una orden de la app, el nodo llama a la misma función aplicar(). Así, el interruptor y la app siempre alternan a partir del estado real.

Avisar al hub

Con el mensaje de la parte 4, basta un tipo nuevo para los cambios que nacen en el nodo:

void avisarAlHub(bool on) {
  Mensaje m = { 3, 1, (uint8_t)on, 0 };  // tipo 3 = cambio local, salida 1
  esp_now_send(macHub, (uint8_t*)&m, sizeof(m));
}

El hub tiene que aceptar ese aviso aunque no haya pedido nada. Si solo acepta respuestas a sus propias órdenes, el cambio del interruptor nunca llega a la app.

Que la nube no deshaga el cambio

Aquí está la trampa. El Device Shadow tiene dos partes (parte 5):

Si alguien apaga la luz con el interruptor y el hub solo publica reported: "off", el Shadow queda con desired: "on" y reported: "off". Para AWS IoT eso es una diferencia pendiente: genera un delta y el hub vuelve a encender la luz.

La solución: cuando el cambio nace en el dispositivo, el hub publica desired y reported juntos, con el mismo valor:

void reportarCambioLocal(const char* salida, bool on) {
  JsonDocument doc;
  doc["state"]["desired"][salida]  = on ? "on" : "off";
  doc["state"]["reported"][salida] = on ? "on" : "off";
  char buffer[192];
  size_t n = serializeJson(doc, buffer);
  client.publish(topicUpdate.c_str(), buffer, n);
}

La regla general: si un dispositivo se puede controlar desde un lugar distinto de la nube, al informar un cambio propio tiene que actualizar también lo deseado. Si no, la nube va a intentar deshacerlo.

Monitor Serie del hub: llega un cambio local del interruptor y se publica desired y reported juntos en el Shadow

Salida esperada en el hub cuando alguien mueve el interruptor. Los nombres son de ejemplo.

Un caso que hay que prever

ESP-NOW no garantiza la entrega. Si el hub está apagado justo cuando alguien mueve el interruptor, el aviso se pierde: la luz cambia bien, pero la app puede mostrar el estado anterior. Dos formas de cubrirlo:

Antes de conectar un relé a la red eléctrica

Esta parte del proyecto maneja tensiones peligrosas: 230 V a 50 Hz en España y buena parte de Europa, y entre 110 y 127 V a 60 Hz en México y gran parte de América. Un error puede causar una descarga eléctrica o un incendio.

Más detalles del relé en cómo usar un módulo de relé con ESP32.

El material

ESP32-C3 Super Mini

Para cada nodo: pequeña, barata y con ESP-NOW.

Enlace de afiliado: si compras, el sitio recibe una comisión sin costo extra para ti.

Módulo de relé de 1 canal con optoacoplador

Revisa que admita la tensión y la corriente de tu carga, y si se activa en alto o en bajo. Para conectarlo a la red eléctrica, sigue las advertencias de la sección anterior.

Enlace de afiliado: si compras, el sitio recibe una comisión sin costo extra para ti.

Preguntas frecuentes

¿Puedo usar un pulsador en lugar de un interruptor?

Sí, cambiando la lógica: con un pulsador, solo cuenta una de las dos transiciones (por ejemplo, al presionar, cuando la lectura pasa a LOW). Con un interruptor de dos posiciones, cuentan las dos.

¿Qué pasa si se va la luz?

Al volver la energía, el nodo arranca apagado. Para que recupere el estado correcto, puede pedírselo al hub (que lo tiene del Shadow) o leerlo de su memoria NVS (parte 6).

¿Puedo usar el interruptor de pared existente?

Sí, si cambias su cableado para que, en lugar de cortar la fase, solo una el GPIO con GND, y la fase pase siempre por el relé. Es un cambio en la instalación eléctrica: hazlo con un electricista.

Sigue leyendo