La mayoría de tutoriales de domótica con ESP32 terminan en un LED que se enciende desde el celular. El problema aparece después: cuando tienes diez dispositivos, cuando otra persona de la casa quiere usarlos, cuando quieres controlarlos por voz o cuando un corte de luz desordena todo.
IHouse es la plataforma de domótica que construí y que uso en mi casa. En esta serie te cuento cómo la hice, parte por parte, con las decisiones que tomé, los errores que cometí y el código real. Esta primera parte es el mapa: qué piezas tiene, cómo se hablan entre ellas y por qué elegí cada una.

El hardware de una casa con IHouse: arriba, el maestro (ESP32-S3); abajo, dos esclavos (ESP32-C3).
La arquitectura en una imagen
Las tres capas de IHouse. Los números corresponden a la leyenda y se explican en las secciones siguientes.
El sistema tiene tres capas:
- Dispositivos en la casa: un ESP32-S3 "maestro" y hasta 20 ESP32-C3 "esclavos" que controlan luces o enchufes.
- Nube en AWS: AWS IoT Core como broker MQTT, Cognito para el login, DynamoDB para los datos y Lambda para las APIs.
- Apps: una web app instalable (PWA), servida gratis desde Netlify, y Atajos de Siri para el control por voz.
No hay ningún servidor propio que mantener. Todo son servicios administrados que, con uso doméstico, cuestan centavos al mes.
Capa 1: los dispositivos, con un gateway
La decisión más importante de todo el proyecto fue esta: solo un dispositivo por casa se conecta a internet.
El maestro, un ESP32-S3, es el único que tiene Wi-Fi, TLS y certificado propio. Es el único que existe en AWS IoT como Thing. Los esclavos, ESP32-C3, reciben las órdenes del maestro por ESP-NOW, el protocolo punto a punto de Espressif (flechas 1 y 2 del diagrama).
| Cada dispositivo conectado a AWS | Gateway (lo que hice) | |
|---|---|---|
| Conexiones TLS abiertas | Una por dispositivo | Una por casa |
| Certificados y Things que administrar | Uno por dispositivo | Uno por casa |
| Clientes conectados al router | Todos | Solo el maestro |
| Costo en AWS | Crece con cada dispositivo | Solo el del maestro |
| Memoria que necesita cada placa | La de TLS + MQTT + JSON | La del esclavo es mínima |
Los esclavos ni siquiera se conectan al Wi-Fi de la casa. ESP-NOW solo exige que los dos extremos estén en el mismo canal de radio. Así que el esclavo escanea las redes cercanas, lee en qué canal transmite la red del maestro, sin asociarse ni pedir contraseña, y fija ese canal. Un esclavo no guarda credenciales de Wi-Fi y no ocupa un lugar en el router.
Por qué ESP32-S3 para el maestro y ESP32-C3 para los esclavos
El maestro hace TLS, MQTT, JSON, ESP-NOW y un portal de configuración al mismo tiempo. Usé un ESP32-C3 como maestro de pruebas y funcionó, pero tuve que ampliar el stack de la tarea principal y queda con poco margen para crecer. El ESP32-S3, con dos núcleos y más RAM, corre el mismo firmware sin ajustes.
Los esclavos solo reciben órdenes por ESP-NOW y mueven un relé. Al C3 le sobra capacidad para eso y es más barato. En la parte 5 te cuento el error que me dio el C3 como maestro, y por qué no era lo que parecía.
Un interruptor físico que no depende de internet
Cada esclavo admite un interruptor de pared de dos cables conectado a un GPIO. El esclavo aplica el cambio al instante, sin consultar al maestro ni a la nube, y después avisa al maestro para que la app refleje el nuevo estado. Si se cae internet, la luz sigue funcionando desde la pared, como siempre.
Cómo viaja una orden de punta a punta
El estado de toda la casa cabe en un solo texto corto, el campo code del Device Shadow. Cada par de caracteres es una letra, que identifica a un esclavo, y un dígito, 1 encendido y 0 apagado. "A1B0" significa "esclavo A encendido, esclavo B apagado".
{
"state": {
"desired": { "code": "A1B0" },
"reported": { "code": "A1B0" }
}
}
Elegí este formato porque el maestro es un microcontrolador: recorrer un texto de dos en dos caracteres es mucho más barato que interpretar un JSON anidado con un objeto por dispositivo. Además, 20 esclavos ocupan solo 40 caracteres.
Esto es lo que pasa cuando tocas "Encender" en la app:
- La web app publica el nuevo
codecomodesireden el Device Shadow del maestro. - AWS IoT Core compara
desiredconreportedy envía la diferencia (eldelta) al maestro. - El maestro compara letra por letra con el último estado confirmado para saber qué cambió.
- Por cada letra que cambió, envía un paquete ESP-NOW de 2 bytes (letra y estado) al esclavo que corresponde, y espera su confirmación (ACK).
- El esclavo mueve el relé y responde con el ACK.
- Con todas las confirmaciones, el maestro publica el nuevo
codecomoreported. - La app, suscrita a los cambios del Shadow, actualiza el botón.
El paso 6 es importante: el maestro solo reporta lo que se confirmó. Si un esclavo está desconectado, la app no muestra un estado falso.
Hay un caso más: un esclavo que se reinicia no recuerda su último estado. Al arrancar, envía una consulta especial al maestro (su letra seguida de ?) y el maestro le reenvía el último estado confirmado. Así una luz no queda apagada después de un corte de energía si la app dice que está encendida.
La conexión del maestro con AWS IoT Core y el Device Shadow están explicados, con código, en la parte 3 de esta serie.
Capa 2: la nube en AWS
| Servicio | Para qué lo uso |
|---|---|
| AWS IoT Core | Broker MQTT y Device Shadow: guarda el estado de cada casa aunque el maestro esté desconectado |
| Cognito User Pool | Login con usuario y contraseña. El registro público está desactivado: las cuentas se crean por invitación |
| Cognito Identity Pool | Cambia el token del login por credenciales temporales de AWS para leer DynamoDB, limitadas a los datos de la casa del usuario |
| DynamoDB | Usuarios, dispositivos, roles, permisos y los hashes de las API keys |
| Lambda con Function URL | Cinco funciones: la API de Siri, el panel de administración, las API keys, el alta de maestros nuevos y la firma de la conexión MQTT del navegador |
| AWS CDK | Las funciones Lambda y sus roles IAM definidos en TypeScript |
Tres decisiones de esta capa me ahorraron mucho trabajo:
El navegador se conecta directo a AWS IoT Core, pero no firma la conexión (flechas 4 y 7). La app le pide a una Lambda una URL de WebSocket ya firmada con SigV4 y abre con ella una conexión MQTT. No hay un servidor que reenvíe mensajes: la Lambda solo firma y el tráfico va directo del navegador a IoT Core.
Al principio el navegador firmaba la URL con las credenciales temporales de Cognito. El WebSocket abría, pero AWS cerraba la conexión MQTT en silencio, sin CONNACK y sin dejar rastro en CloudWatch. Firmar desde una Lambda con credenciales de sts:AssumeRole lo resolvió, y además resultó más seguro. Te cuento esa depuración en la parte 10.
Una sola consulta a DynamoDB por sesión (flechas 3 y 5). La tabla usa userId como partition key y deviceId como sort key, así que un solo Query devuelve todos los dispositivos de la casa. La app lo hace al iniciar sesión y guarda el resultado en memoria.
Una web app sin hosting en AWS. La PWA es un sitio estático y la sirvo desde el plan gratuito de Netlify. No hay ningún acoplamiento con AWS: las Lambdas tienen CORS abierto, Cognito no restringe por dominio y el WebSocket de IoT Core no pasa por CORS.
El aislamiento entre casas
Cuando IHouse pasó de mi casa a varios clientes, apareció un problema: los permisos tenían escrito el nombre de mi Thing. Cada cliente nuevo obligaba a editarlos a mano, y un comodín habría dado a cada usuario acceso a las casas de los demás.
La solución tiene dos partes, y ninguna confía en lo que envía el navegador:
- La conexión MQTT. La Lambda que firma la URL verifica el token del usuario, saca de ahí el nombre de su Thing y firma con una política de sesión que solo permite ese Thing y un client ID de un solo uso. Si la URL se filtra, no sirve para otra casa.
- La base de datos. Los permisos de la Identity Pool usan Principal Tags: variables como
${aws:PrincipalTag/householdUserId}que IAM reemplaza en cada sesión con los datos del usuario. Así elQuerysolo puede leer la partición de su propia casa.
Lo explico paso a paso en la parte 13.
Capa 3: las apps
La web app está hecha con React, Vite y TypeScript, y es una PWA: se instala en la pantalla de inicio del celular como una app nativa. Muestra los dispositivos de la casa y el estado de conexión. Con los roles que agregué después, también tiene un panel para administrar integrantes.
Siri no puede firmar peticiones con SigV4, así que no puede hablar directo con AWS IoT Core. Para la voz hice una API en Lambda con su propia API key por usuario (flecha 6). La Lambda busca el hash de la key en DynamoDB y obtiene de ese registro el Thing y los dispositivos que esa persona puede controlar, nunca de la petición. Después lee el code actual, cambia solo las letras pedidas y actualiza el Shadow. Un Atajo de iOS hace el POST, y "Oye Siri, enciende la sala" funciona.
Lo que cambió en el camino
La arquitectura de la imagen no es la primera versión. Estos son los cambios más grandes, cada uno con su parte en la serie:
- Agregar dispositivos sin reflashear. Al principio, la tabla que relaciona cada letra con la MAC de su esclavo estaba escrita en el firmware del maestro. Cada esclavo nuevo exigía compilar y flashear el maestro otra vez. Ahora la tabla viaja en el propio Shadow y el maestro la actualiza al vuelo.
- Configurar dispositivos sin cable. Cada esclavo nuevo levanta su propia red Wi-Fi con un portal de configuración. El maestro recibe su identidad (certificado incluido) con un código de reclamo de un solo uso: ya no hay credenciales quemadas en el firmware.
- Varias personas por casa. El titular puede invitar hasta 5 integrantes y decidir qué dispositivo controla cada uno. Los integrantes no escriben el Shadow directamente: sus órdenes pasan por una Lambda que valida sus permisos. Un cambio de permisos llega a la app al instante, por MQTT.
- Flashear desde el navegador. Instalar el firmware ya no requiere Arduino IDE: se hace desde la web app con un botón.
Cuánto cuesta
Para uso doméstico, centavos de dólar al mes:
| Servicio | Por qué cuesta tan poco |
|---|---|
| AWS IoT Core | Un maestro por casa y unos pocos mensajes al día |
| Cognito | Cobra por usuario activo al mes, no por sesión, y la capa gratuita cubre miles de usuarios |
| DynamoDB | Modo bajo demanda, una consulta por sesión |
| Lambda | Una invocación por comando de voz o por conexión; el primer millón al mes es gratis |
| Netlify | La web app es un sitio estático en el plan gratuito |
El gateway es lo que mantiene el costo bajo: los esclavos no generan ni un mensaje en AWS. En la última parte de la serie hago las cuentas completas, servicio por servicio, también para escenarios con muchas casas.
Los límites de este diseño
- Máximo 20 esclavos por maestro. Es un límite fijo del SDK de ESP-NOW (20 peers por dispositivo). Una casa más grande necesita un segundo maestro, y el modelo de datos ya lo admite.
- El control remoto depende de internet. Sin conexión, la app y Siri no funcionan; los interruptores físicos sí.
- Si el maestro está apagado cuando alguien mueve un interruptor, ese aviso se pierde y la app puede mostrar un estado viejo hasta el siguiente cambio. Es un problema conocido que todavía no resolví.
Preguntas frecuentes
¿Por qué no usar Home Assistant?
Home Assistant es excelente si quieres domótica local en tu propia casa. Yo quería otra cosa: una plataforma para varias casas, con usuarios, permisos y control remoto sin abrir puertos ni mantener un servidor en cada hogar. Para eso, una arquitectura en la nube tenía más sentido.
¿Por qué ESP-NOW y no Wi-Fi para todos los dispositivos?
Porque cada dispositivo con Wi-Fi y TLS necesita su certificado, su conexión permanente con la nube y un lugar en el router. Con ESP-NOW los esclavos solo necesitan estar en el mismo canal de radio que el maestro, sin router ni contraseña.
¿Necesito AWS para seguir la serie?
Para las partes de la nube, sí. Las partes de firmware (ESP-NOW, NVS, portal cautivo, el interruptor físico) sirven para cualquier proyecto con ESP32, aunque uses otra nube o ninguna.
¿Qué viene en la serie?
El hardware (parte 2), la conexión con AWS IoT Core (parte 3), ESP-NOW paso a paso (parte 4) y, después, la base de datos, la API de Siri, la web app, la seguridad y los costos. El índice completo está en la página de la serie.