¿Cuántas veces al día abrís la app de Home Assistant solo para chequear si el CO₂ de la pieza donde estás trabajando sigue en verde? En un hogar con sensores repartidos, tener que sacar el celular o abrir un tab del navegador cada vez que quieres un dato rompe el flujo. La solución elegante es un panel dedicado, siempre visible, con consumo mínimo: una pantalla e-Paper de 4,26" pegada a la pared.

En esta guía vas a armar ese panel usando la placa driver EE05 de Seeed Studio (que integra el XIAO ESP32-S3 Plus). Al final vas a tener un dispositivo autónomo que consulta la REST API de Home Assistant cada 15 minutos, muestra temperatura, humedad, CO₂, formaldehído y VOC del sensor de calidad de aire, y duerme el resto del tiempo consumiendo apenas microamperios.

Concepto: por qué e-Paper y por qué REST API

Antes de saltar al armado conviene entender por qué la combinación e-Paper + REST API funciona tan bien para este caso y qué alternativas quedan descartadas.

e-Paper vs OLED/TFT: la pantalla de tinta electrónica tiene una propiedad llamada bistabilidad. Una vez dibujada la imagen, el panel la mantiene sin consumir corriente. Sí, sin corriente. La actualización solo consume energía; el reposo consume prácticamente cero. Para información que cambia cada 15 minutos, es la tecnología correcta. Un OLED o TFT del mismo tamaño consumiría 100+ mA continuos y se degradaría por burn in.

REST API vs MQTT vs WebSocket: Home Assistant expone tres mecanismos para lecturas externas. MQTT es más eficiente si vas a leer decenas de sensores en tiempo real; WebSocket es lo que usa el frontend oficial. Pero para leer 5 valores cada 15 minutos, la REST API es la más simple: una URL, un token y un GET por sensor. Sin brokers, sin subscripciones, sin state machines.

e-Paper vs display gráfico con gráfico 24h: podrías graficar histórico si quisieras (ver "Variantes"), pero para el uso base los números grandes son más rápidos de leer de un vistazo.

Hardware necesario

  • XIAO ESP32-S3 Plus: núcleo del sistema. WiFi + Bluetooth + más RAM que un ESP32 clásico, ideal para manejar la imagen de fondo en memoria.
  • Placa EE05 de Seeed Studio: driver para pantallas e-Paper. Trae socket para el XIAO ESP32-S3 Plus, conector para batería LiPo con circuito de carga, y un flat de 24 pines para la pantalla.
  • Pantalla e-Paper 4,26" monocromo de Seeed Studio: 800×480 píxeles, controlador SSD1677. Excelente contraste bajo luz natural.
  • Sensor de calidad de aire compatible con Home Assistant (por WiFi vía integración Tuya, Zigbee, o directo por ESPHome). Ver más abajo la variante recomendada con SCD41 directo.
  • Cable USB-C para programar la EE05.
  • Batería LiPo 3,7 V 500-1000 mAh (opcional, para operación sin cable).

Placa driver EE05 con el XIAO ESP32-S3 Plus montado y la pantalla e-Paper de 4,26 pulgadas conectada

Home Assistant: preparar el endpoint

En Home Assistant el sensor de calidad de aire aparece como cinco entidades separadas (una por variable medida). Los identificadores dependen de cómo lo dio de alta la integración; en el ejemplo son:

  • sensor.calidad_de_aire_temperature
  • sensor.calidad_de_aire_humidity
  • sensor.calidad_de_aire_carbon_dioxide
  • sensor.calidad_de_aire_formaldehyde
  • sensor.calidad_de_aire_volatile_organic_compounds

Para consultarlas desde afuera se usa la REST API que Home Assistant expone en el mismo puerto que la interfaz web (por defecto :8123). El endpoint por entidad es:

Código
http://IP_ADDRESS:8123/api/states/<entity_id>

Antes de consumirla necesitas un Long Lived Access Token. Se crea desde el perfil del usuario en Home Assistant, en la sección de seguridad. Copiá el token largo. lo vas a pegar en el código y no lo vas a poder volver a ver.

Un test rápido desde tu terminal antes de programar el ESP32:

Código
curl -H "Authorization: Bearer TU_TOKEN" \
     -H "Content-Type: application/json" \
     http://192.168.1.50:8123/api/states/sensor.calidad_de_aire_temperature

La respuesta viene en JSON. El campo que te interesa es state, que trae el valor como string.

Ejemplo de dashboard de Home Assistant con tarjetas de calidad de aire. los datos que vamos a replicar en el panel e-Paper

Código del firmware

El firmware usa Seeed_GFX (la fork que Seeed mantiene de TFT_eSPI, ajustada para sus paneles e-Paper). Antes de compilar tienes que generar dos archivos con las herramientas online de Seeed:

  1. driver.h. configuración del combo pantalla + placa. Para el 4,26" con la EE05:
Código
#define BOARD_SCREEN_COMBO 506 // 4.26 inch monochrome ePaper Screen (SSD1677)
#define USE_XIAO_EPAPER_DISPLAY_BOARD_EE05
  1. image.h. la imagen de fondo del panel convertida a bitmap monocromo 1 bpp con el dithering de Floyd Steinberg. Se genera desde SenseCraft HMI Dither Tool.

Con esos dos archivos en la carpeta del sketch, el código completo es:

C++
/*
 * Autor: Ernesto Tolocka (Profe Tolocka)
 * Fecha: 2026-May-17
 * Descripción: Muestra valores de calidad del aire desde Home Assistant.
 * Notas: Usa la placa EE05 y una pantalla EPD monocromática de 4,26 pulgadas
 *
 * Licencia: MIT
 */

#include <WiFi.h>
#include "TFT_eSPI.h"        //Libreria Seeed_GFX para el ePaper

#include <HTTPClient.h>
#include <ArduinoJson.h>    // Usado para interpretar JSON

#include <esp_sleep.h>    // Para sueño profundo

#include "image.h"   // Imagen de fondo

EPaper epaper;

// Nombre y contraseña de la red WiFi
const char* ssid = "--tu red--";
const char* password = "--tu contraseña--";

// Tiempo de actualización (minutos)
const int RefreshTime = 15;

// Token proporcionado por Home Assistant
const char* Token = "PEGAR_AQUI_EL_TOKEN_DE_ACCESO_DE_LARGA_DURACION_DE_HOME_ASSISTANT";

// Función para poner el ESP32 en sueño profundo
void sleepSeconds (uint32_t s) {

  if (s < 60) s = 60;    // Por las dudas

  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);

  esp_sleep_enable_timer_wakeup((uint64_t)s * 1000000ULL);
  esp_deep_sleep_start();
  delay ((uint64_t)s * 1000000ULL);
}

// Función para conectarse a WiFi con tiempo máximo de espera
bool connectWiFi(uint32_t timeoutMs) {

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  uint32_t t0 = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - t0) < timeoutMs) {
    delay(250);
    Serial.print(".");
  }
  Serial.println();
  return WiFi.status() == WL_CONNECTED;
}

// Función para obtener estados desde Home Assistant
String getHAState(String entity_id) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Error! WiFi no conectado");
    return "--";
  }

  WiFiClient client;
  HTTPClient http;

  String url = "http://192.168.100.142:8123/api/states/" + entity_id;

  Serial.println();
  Serial.print("Enviando solicitud a: ");
  Serial.println(url);

  http.setTimeout(5000); // 5 segundos

  if (!http.begin(client, url)) {
    Serial.println("Error en http.begin()");
    return "--";
  }

  http.addHeader("Authorization", String("Bearer ") + Token);
  http.addHeader("Content-Type", "application/json");

  int httpCode = http.GET();

  Serial.print("HTTP code: ");
  Serial.println(httpCode);

  if (httpCode != HTTP_CODE_OK) {
    String errBody = http.getString();
    Serial.println("Error body:");
    Serial.println(errBody.substring(0, 200));

    http.end();
    return "--";
  }

  String payload = http.getString();

  Serial.print("Tamaño del payload: ");
  Serial.println(payload.length());

  http.end();

  JsonDocument doc;

  DeserializationError error = deserializeJson(doc, payload);

  if (error) {
    Serial.print("Error al parsear JSON: ");
    Serial.println(error.c_str());
    return "--";
  }

  String value = doc["state"].as<String>();

  Serial.print("Valor de State: ");
  Serial.println(value);

  return value;
}

void setup() {
  Serial.begin(115200);

  Serial.println ("\nArrancando..");

  if (!connectWiFi(30000)) {
    Serial.println("WiFi no conecta. Yendo a dormir por 5 min.");
    sleepSeconds(300);
    return;
  }
  Serial.println("WiFi conectado!");

  // Configura e inicializa la pantalla
  epaper.begin();
  epaper.fillScreen(TFT_WHITE);          // Limpia la pantalla
  epaper.setTextFont (6);
  epaper.setRotation(0);
  epaper.setTextDatum(MC_DATUM);         // Establece alineación centrada

  epaper.setTextSize (1);

  // Carga la imagen de fondo
  epaper.drawBitmap(0, 0, background, 800, 480, TFT_BLACK);


  // Accede a la REST API de Home Assistant
  String temp = getHAState("sensor.calidad_de_aire_temperature");
  String hum  = getHAState("sensor.calidad_de_aire_humidity");
  String co2  = getHAState("sensor.calidad_de_aire_carbon_dioxide");
  String hcho = getHAState("sensor.calidad_de_aire_formaldehyde");
  String voc  = getHAState("sensor.calidad_de_aire_volatile_organic_compounds");


  epaper.drawString (temp, 90, 290);
  epaper.drawString (hum,  244, 290 );
  epaper.drawNumber (co2.toInt(),  398, 290);
  epaper.drawString (hcho, 556, 290 );
  epaper.drawString (voc,  710, 290 );

  epaper.update ();

  Serial.println ("A dormir...");

  // Demora para permitir el acceso desde el IDE si fuera necesario.
  delay (10000);

  sleepSeconds (RefreshTime * 60);
}

void loop() {

  // No hace nada

}

Cómo pensar el flujo del programa

El código está organizado como una máquina de bajo consumo típica de ESP32:

  1. Setup: única pasada de código útil. Se conecta al WiFi, inicializa el e-Paper, dibuja fondo, consulta las 5 entidades, dibuja los valores y refresca la pantalla.
  2. Deep sleep: al terminar, apaga la radio WiFi, dispara el timer del RTC del ESP32 y entra en deep sleep. El consumo cae a menos de 20 µA.
  3. Wake up por timer: pasados los 15 minutos, el ESP32 se reinicia desde cero. Vuelve a hacer setup(). Loop() nunca se ejecuta.

Por eso loop() está vacío: no vale la pena tener el ESP32 despierto entre lecturas, y el e-Paper mantiene la imagen sin corriente gracias a la bistabilidad.

Punto crítico: sleepSeconds chequea que el intervalo sea al menos 60 segundos. Si por error configurás RefreshTime = 0, el ESP32 despertaría y volvería a dormir en loop. ese piso te protege el firmware de un bug tuyo.

Panel armado en su gabinete impreso en 3D, montado sobre la pared del taller

Variantes y mejoras

Tres extensiones concretas que no están en el tutorial original y que valen la pena:

  • Sensor SCD41 directo, sin Home Assistant: si quieres un panel autónomo sin depender de HA, conecta un SCD41 (I²C, mide CO₂ real por NDIR + temperatura + humedad) directamente al XIAO ESP32-S3 Plus. Elimina la dependencia del WiFi para leer, y puedes usar el WiFi solo para reportar histórico. Precisión de CO₂ mucho mejor que los sensores Tuya de bajo costo (que suelen ser VOC extrapolados a "CO₂ equivalente").
  • Gráfico de tendencia de 24 horas: aprovechá los 800×480 de la pantalla. Guardá una lectura por hora en RAM (o en la partición NVS del ESP32 para que sobreviva al deep sleep) y dibujá un mini gráfico de 24 puntos abajo de cada valor. Con los 5 valores tienes 5 sparklines chicos y el número grande arriba.
  • Alarma activa con LED o buzzer: si el CO₂ supera 1200 ppm o el HCHO cruza umbral OSHA, encendé un LED rojo pegado al panel o activá un buzzer piezoeléctrico corto. La pantalla e-Paper no llama la atención por sí sola. un canal auxiliar sí.

Personalización para Chile

Todo el hardware del núcleo del proyecto se puede conseguir localmente en Chile:

  • Placa EE05 driver e-Paper con XIAO ESP32-S3 Plus incluido. la pieza más específica, buscala en MechatronicStore
  • Pantalla e-Paper 4,26" 800×480 monocromo compatible con la EE05 (controlador SSD1677)
  • Sensor SCD41 si tomás el camino independiente (recomendado por precisión)
  • Cable USB-C, batería LiPo 3,7 V 600 mAh, jumpers y caja impresa en 3D para el montaje final

Home Assistant corre sobre cualquier Raspberry Pi 3B+/4/5 (todas se venden en Chile). Si no tienes HA todavía, es un fin de semana bien invertido: documentación oficial de instalación.

Costo aproximado del panel completo (sin contar el sensor de aire ni la Pi con HA): CLP 45.000 - 65.000.

Recursos

Versión chilena basada en el proyecto de Ernesto Tolocka, con re angulación para el ecosistema local y componentes en stock en MechatronicStore.