Cloud, DevOps e IoT en español

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

Device Shadow en AWS IoT: estado deseado y reportado con un ESP32

4 de octubre de 2026 · Steven Carvajal · Tutoriales

Código de este artículo en GitHub →

En la parte 3 conectamos un ESP32 a AWS IoT Core y encendimos un LED cambiando un valor en la nube. Detrás de eso está el Device Shadow, la pieza que hace que un sistema IoT sea fiable: guarda lo que quieres que pase y lo que de verdad pasó, aunque el dispositivo esté desconectado.

En esta parte vemos cómo funciona por dentro, cómo diseñar el documento y cómo manejar varias salidas (luces, enchufes) desde un mismo ESP32.

Qué es el Device Shadow

Es un documento JSON que AWS IoT guarda por cada dispositivo (Thing). Tiene dos secciones principales:

SecciónQuién la escribeSignificado
desiredLa app, un script, una LambdaLo que se quiere que pase
reportedEl dispositivoLo que el dispositivo confirma que pasó

Cuando un campo de desired no coincide con reported, AWS calcula la diferencia, el delta, y se la envía al dispositivo. El dispositivo la aplica y actualiza reported. Cuando ambos coinciden, el delta desaparece.

{
  "state": {
    "desired":  { "sala": "on",  "cocina": "off" },
    "reported": { "sala": "off", "cocina": "off" },
    "delta":    { "sala": "on" }
  },
  "version": 42
}

La gran ventaja frente a tópicos MQTT propios: si el dispositivo estaba apagado, la orden no se pierde. Al volver, pide el documento y se pone al día.

Los tópicos reservados

Todo el Shadow funciona sobre tópicos MQTT que empiezan por $aws/things/<thing>/shadow/:

TópicoDirecciónPara qué
updatePublicasCambiar desired o reported
update/acceptedRecibesAWS aceptó el cambio
update/rejectedRecibesAWS lo rechazó, con el motivo
update/deltaRecibesHay diferencias entre deseado y reportado
update/documentsRecibesEl documento completo antes y después de cada cambio
getPublicas (cuerpo vacío)Pedir el documento completo
get/accepted / get/rejectedRecibesLa respuesta

El dispositivo normalmente solo necesita publicar en update y get, y suscribirse a update/delta, update/rejected y get/accepted. Cada tópico que use tiene que estar permitido en su política: si falta uno, AWS cierra la conexión (parte 13).

Clásico o con nombre

Cada Thing tiene un Shadow clásico (sin nombre) y puede tener Shadows con nombre: documentos independientes, por ejemplo uno para el estado de las luces y otro para la configuración. Sus tópicos llevan el nombre: $aws/things/<thing>/shadow/name/<nombre>/update.

Separar en varios Shadows tiene dos ventajas: cada documento es más pequeño, y un cambio de configuración no genera deltas en el de estado.

Cómo diseñar el documento

Varias salidas desde un ESP32

En la parte 3 el código buscaba una sola clave, led. Con varias salidas, el ESP32 recorre las claves del delta y aplica cada una. Este fragmento sustituye a la función onMessage de la parte 3:

struct Salida {
  const char* nombre;  // clave en el Shadow
  uint8_t pin;
};

Salida salidas[] = {
  { "sala",   4 },
  { "cocina", 5 },
};

void aplicarYReportar(JsonObject cambios) {
  JsonDocument reporte;
  JsonObject reported = reporte["state"]["reported"].to<JsonObject>();

  for (JsonPair kv : cambios) {
    for (Salida& s : salidas) {
      if (strcmp(kv.key().c_str(), s.nombre) != 0) continue;
      bool encender = strcmp(kv.value() | "", "on") == 0;
      digitalWrite(s.pin, encender ? HIGH : LOW);
      reported[s.nombre] = encender ? "on" : "off";  // solo lo que se aplicó
    }
  }
  if (reported.size() == 0) return;  // nada que reportar

  char buffer[512];
  size_t n = serializeJson(reporte, buffer);
  client.publish(topicUpdate.c_str(), buffer, n);
}

void onMessage(String& topic, String& payload) {
  JsonDocument doc;
  if (deserializeJson(doc, payload)) return;

  if (topic == topicDelta) {
    aplicarYReportar(doc["state"].as<JsonObject>());
  } else if (topic == topicGetAccepted) {
    aplicarYReportar(doc["state"]["desired"].as<JsonObject>());
  }
}

Tres detalles:

  1. Se reporta solo lo aplicado. Una clave desconocida en desired no aparece en reported, así el delta sigue mostrando que algo no se cumplió.
  2. Al conectar se pide el documento completo (get), como en la parte 3, y se aplica desired entero. Así el dispositivo se pone al día aunque se haya perdido algún delta.
  3. Un reporte por mensaje, con todas las salidas que cambiaron, en lugar de un publish por salida.

Si las salidas no están en el mismo ESP32 sino en nodos ESP-NOW, la idea es la misma: el hub envía la orden al nodo y solo reporta cuando el nodo confirma (parte 4).

Cambiar y leer el Shadow desde fuera

Desde la terminal, para cambiar el estado deseado:

aws iot-data update-thing-shadow --thing-name mi-esp32 \
  --cli-binary-format raw-in-base64-out \
  --payload '{"state":{"desired":{"sala":"on"}}}' salida.json

Y para leerlo:

aws iot-data get-thing-shadow --thing-name mi-esp32 shadow.json
cat shadow.json

Una app web o una función Lambda hacen lo mismo con el SDK de AWS (UpdateThingShadow y GetThingShadow), o publicando directamente en los tópicos por MQTT (parte 10).

Errores comunes

update/rejected con código 400. El JSON está mal formado o no tiene la estructura {"state": {...}}. El mensaje de rechazo dice qué campo falla.

Código 409: conflicto de versión. Si envías "version" en la actualización y no coincide con la actual, AWS la rechaza. Es útil para evitar sobrescribir cambios ajenos; si no lo necesitas, no envíes la versión.

Código 413: documento demasiado grande. Superaste los 8 KB. Separa en Shadows con nombre o reduce las claves.

El delta no llega al reconectar. Pide el documento con get después de suscribirte, en lugar de esperar un delta.

El ESP32 no recibe get/accepted. Revisa dos cosas: que la política permita suscribirse a ese tópico, y que el buffer de la librería MQTT sea suficiente para el documento completo, que incluye metadatos.

¿Cuánto cuesta?

Las operaciones del Shadow (cada update, get o delete) se cobran aparte de los mensajes MQTT, por millón de operaciones, y para una casa son fracciones de centavo al mes. Revisa la página de precios de AWS IoT Core para tu región; la estimación completa está en la parte 22.

Preguntas frecuentes

¿El Shadow sustituye a los mensajes retenidos de MQTT?

Para estado, sí, y con más funciones: separa deseado de reportado, calcula el delta y tiene versión. AWS IoT Core también admite mensajes retenidos, útiles para avisos simples.

¿Puede escribir en desired el propio dispositivo?

Puede, pero no conviene: desired es la intención del usuario. Si el dispositivo cambia por su cuenta (un interruptor físico), debe reportarlo en reported y, si quieres que la app no lo revierta, actualizar también desired.

¿Cuántos Shadows puede tener un dispositivo?

Uno clásico y varios con nombre. Revisa las cuotas actuales de AWS IoT para el número máximo.

Sigue leyendo