Integrar el reTerminal E1001 en HA

Integrar el reTerminal E1001 en HA

En esta entrada te enseño a integrar el reTerminal E1001 en HA, la mejor pantalla de tinta electrónica que hemos probado hasta ahora.

reTerminal E1001, ¿la pantalla perfecta?

La reTerminal E1001 es la nueva pantalla de tinta electrónica lanzada por Seeed Studio, distribuidor oficial de productos de Home Assistant y un fabricante bien conocido en nuestra comunidad por productos como el ReSpeaker Lite.

Se trata de una pantalla en blanco y negro de 7,5″ (800 × 480 pixel), que corre sobre una placa ESP32-S3 con 8 MB de PSRAM (compatible con ESPHome) y tiene una batería incorporada de 2000 mAh (que proporciona hasta 3 meses de autonomía, actualizándose cada 6h). Además admite tarjetas Micro SD de hasta 32GB e incorpora conectividad WiFi y Bluetooth, botones físicos, sensor de temperatura y humedad, micrófono, zumbador y un puerto de expansión (puedes consultar el ‘pinout’ en su documentación oficial).

Sólo por los componentes que incorpora, ya se trata de un dispositivo superior al resto de pantallas de tinta electrónica que hemos probado hasta ahora. Pero además de esto hay otros dos aspectos que la desmarcan de otras pantallas.

El primero es que se nota un acabado premium con una carcasa metálica, que viene preparada para ser colgada en la pared o colocada en tu escritorio con el pie que incluye el pack. Por lo tanto, puedes usarla desde que la sacas de la caja (sin necesidad de comprar marcos o una carcasa impresa en 3D).

El segundo es que, como exploraremos a continuación, es una pantalla «open-source» muy fácil de utilizar y compatible con distintas plataformas (incluyendo, por supuesto, Home Assistant). Mientras que el resto de pantallas que hemos usado hasta ahora requerían ciertos conocimientos de ESPHome, esta pantalla puede ser utilizada aunque no tengas ni idea de programación.

*Algún precio puede haber cambiado desde la ultima revision

Plataformas compatibles

Antes de integrar el reTerminal E1001 en HA quiero resaltar que esta pantalla de tinta electrónica también es compatible con otras plataformas. Por ejemplo, si vienes del mundo de Arduino, podrás usar tus conocimientos con ella.

Otra opción muy interesante es utilizar TRMNL, una plataforma muy popular que te permite instalar plugins para mostrar, de una forma muy sencilla, información de tus aplicaciones (calendarios, estadísticas, cotizaciones, listas de tareas… y mucho más).

No obstante, si no quieres pagar ni programar, te recomiendo que utilices SenseCraft HMI. Esta es una plataforma desarrollada por Seeed Studio, que te permite personalizar tu pantalla a través de una interfaz gráfica (sin necesidad de escribir ni una línea de código).

Puedes consultar las instrucciones en la documentación oficial, pero para empezar sólo necesitas seguir estos pasos:

  1. Enciende la pantalla con el interruptor que trae en la parte posterior y en pantalla verás el nombre del punto de acceso WiFi que ha creado. Utiliza tu dispositivo móvil para conectarte a dicho punto y conéctala a tu red WiFi. Como siempre, te recomiendo que asignes una IP fija en tu router para evitar fallos en el futuro si esta cambia.
  2. Cuando se conecte a la red aparecerá en pantalla un código de emparejamiento. Entra en la página de SenseCraft HMI y baja hasta encontrar el botón «Connect Device». Al pulsar sobre el botón tendrás que hacer login con tu cuenta de Seeed Studio, o crear una nueva.
  1. Desde la interfaz principal, pulsa sobre «+ Add Device», dale un nombre a tu pantalla (por ejemplo, «reTerminal E1001») e introduce el código de emparejamiento que aparece en el dispositivo. Inmediatamente el dispositivo quedará conectado emitiendo un sonido.
  2. Si ahora pulsas sobre tu pantalla, accederás a la interfaz de configuración. Fíjate que cargar imágenes en tu pantalla es tan fácil como pulsar en «Add Photos», seleccionar un fichero y hacer clic en «Deploy».
  1. Puedes añadir más imágenes y establecer un intervalo de tiempo (en el campo «Interval»), o cambiar de una a otra utilizando los botones de la parte superior del marco.
  2. También puedes crear una interfaz pulsando en el apartado «Canvas» del menú lateral. Esto abrirá un sencillo panel, al puro estilo «paint» para que la diseñes dibujando formas, textos, imágenes, códigos QR… Y si te da pereza, puedes pedirle a la IA que lo haga por ti! (por ejemplo, «Create a dashboard for my Smart Home»), o utilizar alguno de los «templates» que vienen listos para usar (como el del tiempo de tu ciudad).
  3. Además puedes añadir distintas páginas (pulsando sobre «+ Add Page» en la parte superior), para que vaya cambiando la información de tu pantalla. También me ha gustado mucho que tengas la posibilidad de exportar e importar tus diseños a un fichero JSON, para poder compartirlos.

Integración con Home Assistant

Ahora si, vamos a integrar el reTerminal E1001 en HA y para ello te voy a enseñar dos formas de hacerlo.

Integración con Puppet

Puppet es un complemento de Home Assistant que, básicamente, hace una captura de pantalla de un dashboard y la expone como imagen para que la puedas mostrar en tu ‘display’.

Esto tiene como gran ventaja que no requiere que diseñes la pantalla programando en ESPHome, ya que puedes hacerlo creando tarjetas desde el panel de control. Siguiendo las instrucciones de la documentación oficial, puedes integrar el reTerminal E1001 en HA de la siguiente forma:

  1. En Home Assistant, haz clic sobre tu nombre de usuario y ve al apartado «Seguridad». Baja hasta el final de la pantalla y haz clic en «Crear token». Dale un nombre (por ejemplo, «Puppet») y consérvalo.
  2. Pulsa en este enlace para instalar el complemento Puppet. Se abrirá una nueva ventana y primero tendrás que añadir el repositorio, para luego pulsar en «Instalar».
  3. Cuando finalice la instalación accede a la pestaña de «Configuración», copia el token generado en el primer paso en el campo «access_token» y pulsa en «Guardar»
  4. Vuelve a la pestaña de «Información», pulsa en «Iniciar» y accede a la pestaña «Registro» para verificar que se ha iniciado sin errores. En la consola verás que el complemento te expone esta URL que corresponde a la imagen de tu panel de control.

http://homeassistant.local:10000/lovelace/0?viewport=1000x1000
  1. Observa que puedes modificar la URL de la siguiente forma, para adaptar la resolución y mostrarla en blanco y negro (adaptándola las características de nuestra pantalla). También puedes reemplazar el ‘0’ por la ruta al panel que quieres mostrar.
http://homeassistant.local:10000/lovelace/0?viewport=800x480&eink=2
  1. En Home Assistant, ve a tu complemento de ESPHome, pulsa en “New device” y “Continue”. Dale un nombre a tu dispositivo (por ejemplo, “reTerminal 1001”) y pulsa en “Next”.
  2. Como tipo de dispositivo selecciona “ESP32-S3”. Observarás de fondo que se ha creado un nuevo bloque para tu dispositivo. Pulsa en “Skip” y haz clic en “Edit”, sobre el bloque de tu dispositivo.
  3. Añade las siguientes líneas al final de tu código, adaptando la URL si fuera necesario:
http_request:
  verify_ssl: false
  timeout: 10s
  watchdog_timeout: 15s
online_image:
  - id: dashboard_image
    format: PNG
    type: BINARY
    buffer_size: 30000
    url: http://homeassistant.local:10000/lovelace/0?viewport=800x480&eink=2&invert  # Replace with your Home Assistant address
    update_interval: 30s
    on_download_finished:
      - delay: 0ms
      - component.update: epaper_display
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never
    lambda: |-
      it.image(0, 0, id(dashboard_image));
  1. Pulsa en “Save” e “Install”. Selecciona “Manual download” y espera a que se compile el código. Cuando termine, selecciona la opción “Modern format” para descargar el fichero ‘.bin’ correspondiente.
  2. Conecta la pantalla con el cable USB-C de datos por el puerto que trae en un lateral a tu ordenador. Ahora ve a la página de ESPHome y pulsa en “Connect”. En la ventana emergente selecciona tu dispositivo y pulsa en “Conectar”.
  3. Ahora pulsa en “Install” y selecciona el fichero ‘.bin’ obtenido en el paso 9. De nuevo, pulsa en “Install”.
  4. Vuelve a Home Assistant y ve a Configuración > Dispositivos y servicios. Lo normal es que tu dispositivo haya sido descubierto y aparezca en la parte superior, esperando únicamente a que pulses el botón de “Configurar”. De lo contrario pulsa en el botón de “Añadir integración”, busca “ESPHome” e introduce la IP de tu placa en el campo ‘Host’.  

Integración con ESPHome

Por último voy a enseñarte cómo integrar el reTerminal E1001 en HA aprovechándo todo su potencial. Es cierto, es un poco más laborioso, pero tratándose de la pantalla de tinta electrónica que hemos tenido, no podíamos desarpovecharla.

Basándonos en la documentación oficial, he creado un código que te permite lo siguiente:

  • Leer el sensor de temperatura y humedad, tanto desde la pantalla como desde HA.
  • Monitorizar la batería del dispositivo, tanto desde la pantalla como desde HA.
  • Crear rápidamente una interfaz de bienvenida con fecha, hora y una imagen personalizable (sin programar).
  • Crear distintas páginas y navegar entre ellas usando los botones blancos.
  • Configurar fácilmente el ‘sleep mode’ para maximizar la duración de la batería.
  • Actualizar manualmente el contenido de la pantalla usando el botón verde.
  • Tener control absoluto del zumbador, para emitir politonos desde Home Assistant.
  • Además puedes usar los ejemplos como base para personalizar tu pantalla.

Si quieres decantarte por esta opción, sigue estos pasos:

  1. Conecta la pantalla a tu ordenador a través del puerto USB-C.
  2. En Home Assistant, accede al complemento de ESPHome desde el menú lateral y pulsa en “New device”. Pulsa en “Continue” y dale un nombre (por ejemplo, “reTerminal 1001”).
  3. Pulsa en “Next” y a continuación selecciona la opción “ESP32-S3” como tipo de dispositivo. Verás que en el fondo se ha creado un nuevo dispositivo.
  4. Pulsa en “Skip” y haz clic en “Edit”, sobre el bloque de tu dispositivo. Copia el código que aparece y consérvalo, ya que vas a necesitar alguna parte del mismo.
  5. Copia el siguiente código y úsalo para reemplazar el código anterior en ESPHome.
substitutions:
# Device customization
# Personalización del dispositivo
  name: reterminal-1001
  friendly_name: reTerminal 1001
  main_image: https://aguacatec.es/wp-content/uploads/2025/09/terminal.png
  pages: 3
# Sleep mode configuration
# Configuración del sleep mode
  run_duration: 60s 
  sleep_duration: 3600s 
# Examples
# Ejemplos
  weather: weather.openweathermap
  calendar: calendar.aguacatec
  telegram_members: input_number.miembros_telegram
################################################################################################################
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: 600
    then:
      - output.turn_on: bsp_battery_enable
      - delay: 200ms
      - component.update: battery_voltage
      - component.update: battery_level
esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
  encryption:
    key: "asdfsdsdf6465/aeT/Y+1slF9CIaknds9823"
  services:
    - service: play_rtttl
      variables:
        song_str: string
      then:
        - rtttl.play:
            rtttl: !lambda 'return song_str;'
ota:
  - platform: esphome
    password: "sdds6451637e461003derdfg456f"
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Reterminal-1001 Fallback Hotspot"
    password: "KGdsajtyYTEv76Ka"
captive_portal:
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO4          
      mode: INPUT_PULLUP
      inverted: true
    id: button_2
    name: "Page - Next"
    icon: "mdi:page-next"
    internal: true
    on_press:
      then:
        - lambda: |-
            if (id(page_index) == ${pages}-1 ) {
              id(page_index) = 0;
            }
            else {
              id(page_index) += 1;
            }
            id(epaper_display).update(); 
        
  - platform: gpio
    pin:
      number: GPIO5           
      mode: INPUT_PULLUP
      inverted: true
    id: button_3
    name: "Page - Previous"
    icon: "mdi:page-previous"
    internal: true
    on_press:
      then:
        - lambda: |-
            if (id(page_index) == 0) {
              id(page_index) = ${pages}-1;
            }
            else {
              id(page_index) -= 1;
            }
            id(epaper_display).update();
deep_sleep:
  id: deep_sleep_1
  run_duration: ${run_duration} 
  sleep_duration: ${sleep_duration}  
  wakeup_pin: GPIO3
  wakeup_pin_mode: INVERT_WAKEUP
font:
  - file: "gfonts://Kanit"
    id: font_time
    size: 60
  - file: "gfonts://Kanit"
    id: font_date
    size: 20
  - file: "gfonts://Inter@700"
    id: font_sensor
    size: 22
  - file: "gfonts://Kanit"
    id: font_temperature
    size: 100
  - file: "gfonts://Exo 2"
    id: font_calendar
    size: 25
  - file: "gfonts://Story Script"
    id: font_telegram_title
    size: 45
  - file: "gfonts://Kanit"
    id: font_telegram
    size: 200
globals:
  - id: page_index
    type: int
    restore_value: no
    initial_value: '0'
i2c:
  scl: GPIO20
  sda: GPIO19
image:
  - file: mdi:battery-70
    id: icon_battery
    resize: 25x25
    type: BINARY
    transparency: chroma_key
  - file: mdi:thermometer
    id: icon_temperature
    resize: 25x25
    type: BINARY
    transparency: chroma_key
  - file: mdi:water-percent
    id: icon_humidity
    resize: 25x25
    type: BINARY
    transparency: chroma_key
  - file: ${main_image}
    id: wallpaper
    resize: 800x458
    type: BINARY
    transparency: chroma_key
    invert_alpha: true
  - file: https://aguacatec.es/wp-content/uploads/2025/09/Telegram2.png
    id: telegram
    resize: 400x400
    type: BINARY
    transparency: chroma_key
    invert_alpha: true
  - file: mdi:weather-partly-cloudy
    id: icon_weather
    resize: 200x200
    type: BINARY
    transparency: chroma_key
  - file: mdi:calendar
    id: icon_calendar
    resize: 200x200
    type: BINARY
    transparency: chroma_key
output:
  - platform: ledc
    pin: GPIO45
    id: rtttl_out
    frequency: 1000Hz
  - platform: gpio
    pin: GPIO21
    id: bsp_battery_enable
rtttl:
  output: rtttl_out
  id: my_rtttl
  gain: 60%
sensor:
  - platform: sht4x
    temperature:
      name: "Temperature"
      id: temp_sensor
    humidity:
      name: "Humidity"
      id: hum_sensor
  - platform: adc
    pin: GPIO1
    name: "Battery Voltage"
    id: battery_voltage
    internal: true
    attenuation: 12db
    filters:
      - multiply: 2.0  # Voltage divider compensation
  - platform: template
    name: "Battery Level"
    id: battery_level
    unit_of_measurement: "%"
    icon: "mdi:battery"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    filters:
      - calibrate_linear:
          - 4.15 -> 100.0
          - 3.96 -> 90.0
          - 3.91 -> 80.0
          - 3.85 -> 70.0
          - 3.80 -> 60.0
          - 3.75 -> 50.0
          - 3.68 -> 40.0
          - 3.58 -> 30.0
          - 3.49 -> 20.0
          - 3.41 -> 10.0
          - 3.30 -> 5.0
          - 3.27 -> 0.0
      - clamp:
          min_value: 0
          max_value: 100
  - platform: homeassistant
    id: telegram_members_counter
    entity_id: ${telegram_members}
    internal: true
  - platform: homeassistant
    id: sensor_temperature
    entity_id: ${weather}
    attribute: temperature
    internal: true
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
text_sensor:
  - platform: homeassistant
    id: calendar_event
    entity_id: ${calendar}
    attribute: message
    internal: true
  - platform: homeassistant
    id: calendar_time
    entity_id: ${calendar}
    attribute: start_time
    internal: true
time:
  - platform: homeassistant
    id: ha_time
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: 30s
    lambda: |-
      // DATE AND TIME
      auto now = id(ha_time).now();
      struct tm timeinfo = now.to_c_tm();
      // centering time HH:MM
      char timeStr[6];
      strftime(timeStr, sizeof(timeStr), "%H:%M", &timeinfo);
      it.printf(80, 40, id(font_time), TextAlign::CENTER, timeStr);
      // Date: Day of week
      // const char *weekday[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
      const char *weekday[] = {"Dom","Lun","Mar","Mie","Jue","Vie","Sab"};
      const char *wday = weekday[timeinfo.tm_wday];
      // Date: month - day
      char dateStr[12];
      strftime(dateStr, sizeof(dateStr), "%b %d", &timeinfo);  // e.g. Jun 15
      // Day of the week + date below the time
      it.printf(80, 80, id(font_date), TextAlign::CENTER, "%s, %s", wday, dateStr);
      // SENSOR AND BATTERY LEVEL
      it.image(520, 15, id(icon_temperature));
      it.printf(550, 14, id(font_sensor), "%.0f°C", id(temp_sensor).state);
      it.image(620, 15, id(icon_humidity));
      it.printf(650, 14, id(font_sensor), "%.0f%%", id(hum_sensor).state);
      it.image(710, 15, id(icon_battery));
      it.printf(735, 14, id(font_sensor), "%.0f%%", id(battery_level).state);
      // ----------  PAGE 0  ----------
      if (id(page_index) == 0) {
        // WALLPAPER
        it.image(0, 20, id(wallpaper));
      }
      // ----------  PAGE 1  ----------
      if (id(page_index) == 1) {
        it.image(120, 130, id(icon_weather));
        it.printf(220, 360, id(font_temperature), TextAlign::CENTER, "%.0f°C", id(sensor_temperature).state);
        it.image(460, 130, id(icon_calendar));
        it.printf(560, 350, id(font_sensor), TextAlign::CENTER, "%s", id(calendar_event).state.c_str());
        it.printf(560, 380, id(font_calendar), TextAlign::CENTER, "%s", id(calendar_time).state.c_str());
      }
      // ----------  PAGE 2  ----------
      if (id(page_index) == 2) {
        it.printf(280, 240, id(font_telegram), TextAlign::CENTER, "%.0f", id(telegram_members_counter).state);
        it.print(280, 350, id(font_telegram_title), TextAlign::CENTER, "Miembros en Telegram");
        it.image(400, 50, id(telegram));
      }
  1. Parte importante. En este código no se han incluido las credenciales para que el dispositivo se conecte a tu WiFi y tu instancia de Home Assistant y tienes que incluirlas manualmente. En concreto me refiero a las siguientes líneas del código que has copiado en el paso 4.

# Enable Home Assistant API
api:
  encryption:
    key: "bg6hash6sjdjsdjk02hh0qnQeYVwm123vdfKE8BP5"
ota:
  - platform: esphome
    password: "asddasda27aab65a48484502b332f"
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Assist Fallback Hotspot"
    password: "ZsasdasdHGP2234"
  1. Lo que tienes que hacer es, encontrar las líneas correspondientes en el código (está al principio) y añadir la información correspondiente.
  2. Ahora sí, pulsa en “Save” e “Install”. Selecciona “Manual download” y espera a que se compile el código. 
  3. Cuando termine, selecciona la opción “Modern format” para descargar el fichero ‘.bin’ correspondiente.
  4. Conecta el reTerminal 1001 con el cable USB-C de datos por el puerto que trae en la parte posterior a tu ordenador.
  5. Ahora ve a la página de ESPHome y pulsa en “Connect”. En la ventana emergente selecciona tu placa y pulsa en “Conectar”.
  6. Ahora pulsa en “Install” y selecciona el fichero ‘.bin’ obtenido en el paso 9. De nuevo, pulsa en “Install”.
  7. Vuelve a Home Assistant y ve a Configuración > Dispositivos y servicios. Lo normal es que tu dispositivo haya sido descubierto y aparezca en la parte superior, esperando únicamente a que pulses el botón de “Configurar”. De lo contrario pulsa en el botón de “Añadir integración”, busca “ESPHome” e introduce la IP de tu placa en el campo ‘Host’.  
  8. Listo! Ya has terminado de integrar el reTerminal E1001 en HA y puedes consultar las siguientes entidades!

Trucos para un dashboard bonito

Con esta pantalla lo tienes todo para tener un panel perfectamente integrado con tu decoración. A continuación te dejo unos trucos y otro código de ejemplo para ayudarte a conseguirlo:

  • Combínalo con un marco de madera, como el RÖDALM de Ikea (por menos de 4€).
  • Utiliza alguno de los tipos de letra elegantes disponibles en Google Fonts.
  • Genera una imagen con IA, y pídele que la convierta a blanco y negro, con una escala de grises de 8 colores.
  • Activa el parámetro ‘dither: FLOYDSTEINBERG’ en la imagen de ESPHome, para conseguir mayor resolución.
substitutions:
# Device customization
# Personalización del dispositivo
  name: reterminal-1001
  friendly_name: reTerminal 1001
# Sleep mode configuration
# Configuración del sleep mode
  run_duration: 60s 
  sleep_duration: 21600s # 6 Horas
# Sensors
# Sensores
  weather: weather.openweathermap
  youtube_members: sensor.aguacatec_suscriptores
  telegram_members: sensor.seguidores_aguacatec
  calendar_events: calendar.aguacatec
  calendar_sessions: calendar.aguacatec_sesiones
# Other settings
# Otros ajustes
  allowed_characters: " ¿?¡!#%'()+,-./:µ³°0123456789ABCDEFGHIJKLMNOPQRSTUVWYZabcdefghijklmnopqrstuvwxyzáéíóú"
################################################################################################################
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: 600
    then:
      - delay: 2000ms
      - output.turn_on: bsp_battery_enable
      - delay: 200ms
      - component.update: battery_voltage
      - component.update: battery_level
esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
  encryption:
    key: "ZTRnKpsdf423T/Y+1slF9dsf43"
ota:
  - platform: esphome
    password: "sdf431637e46100324dsf"
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Reterminal-1001 Fallback Hotspot"
    password: "KGwWdsaads6TKa"
captive_portal:
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO4          
      mode: INPUT_PULLUP
      inverted: true
    id: button_2
    name: "Page - Next"
    icon: "mdi:page-next"
    internal: true
        
  - platform: gpio
    pin:
      number: GPIO5           
      mode: INPUT_PULLUP
      inverted: true
    id: button_3
    name: "Page - Previous"
    icon: "mdi:page-previous"
    internal: true
deep_sleep:
  id: deep_sleep_1
  run_duration: ${run_duration} 
  sleep_duration: ${sleep_duration}  
  wakeup_pin: GPIO3
  wakeup_pin_mode: INVERT_WAKEUP
font:
  - file: "gfonts://Kanit"
    id: font_topbar
    size: 20
    glyphs: ${allowed_characters}
  - file: "gfonts://Lobster"
    id: font_quote
    size: 40
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_event
    size: 22
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_start_time
    size: 18
    glyphs: ${allowed_characters}
i2c:
  scl: GPIO20
  sda: GPIO19
image:
# TOPBAR
  - file: mdi:white-balance-sunny
    id: icon_weather
    resize: 25x25
    type: BINARY
    transparency: chroma_key
  - file: mdi:battery-70
    id: icon_battery
    resize: 25x25
    type: BINARY
    transparency: chroma_key
  - file: mdi:thermometer
    id: icon_temperature
    resize: 25x25
    type: BINARY
    transparency: chroma_key
  - file: mdi:water-percent
    id: icon_humidity
    resize: 25x25
    type: BINARY
    transparency: chroma_key
# CALENDAR
  - file: mdi:calendar
    id: icon_calendar
    resize: 50x50
    type: BINARY
    transparency: chroma_key
  - file: mdi:forum-outline
    id: icon_session
    resize: 45x45
    type: BINARY
    transparency: chroma_key
# SOCIAL COUNTER
  - file: mdi:youtube
    id: icon_youtube
    resize: 35x35
    type: BINARY
    transparency: chroma_key
  - file: images/telegram_icon.png
    id: icon_telegram
    resize: 25x25
    type: BINARY
    transparency: chroma_key
# IMAGE
  - file: images/prueba.png
    id: wallpaper
    resize: 500x500
    type: BINARY  
    dither: FLOYDSTEINBERG
    transparency: chroma_key
output:
  - platform: gpio
    pin: GPIO21
    id: bsp_battery_enable
sensor:
  - platform: sht4x
    temperature:
      name: "Temperature"
      id: temp_sensor
    humidity:
      name: "Humidity"
      id: hum_sensor
  - platform: adc
    pin: GPIO1
    name: "Battery Voltage"
    id: battery_voltage
    internal: true
    attenuation: 12db
    filters:
      - multiply: 2.0  # Voltage divider compensation
  - platform: template
    name: "Battery Level"
    id: battery_level
    unit_of_measurement: "%"
    icon: "mdi:battery"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    filters:
      - calibrate_linear:
          - 4.15 -> 100.0
          - 3.96 -> 90.0
          - 3.91 -> 80.0
          - 3.85 -> 70.0
          - 3.80 -> 60.0
          - 3.75 -> 50.0
          - 3.68 -> 40.0
          - 3.58 -> 30.0
          - 3.49 -> 20.0
          - 3.41 -> 10.0
          - 3.30 -> 5.0
          - 3.27 -> 0.0
      - clamp:
          min_value: 0
          max_value: 100
  - platform: homeassistant
    id: sensor_temperature
    entity_id: ${weather}
    attribute: temperature
    internal: true
  - platform: homeassistant
    id: counter_youtube_subscribers
    entity_id: ${youtube_members}
    internal: true
  - platform: homeassistant
    id: counter_telegram_members
    entity_id: ${telegram_members}
    internal: true
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
text_sensor:
  - platform: homeassistant
    id: calendar_event_title
    entity_id: ${calendar_events}
    attribute: message
    internal: true
  - platform: homeassistant
    id: calendar_event_time
    entity_id: ${calendar_events}
    attribute: start_time
    internal: true
  - platform: homeassistant
    id: calendar_session_title
    entity_id: ${calendar_sessions}
    attribute: message
    internal: true
  - platform: homeassistant
    id: calendar_session_time
    entity_id: ${calendar_sessions}
    attribute: start_time
    internal: true
time:
  - platform: homeassistant
    id: ha_time
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: 30s
    lambda: |-
      // DATE AND WEATHER
      auto now = id(ha_time).now();
      struct tm timeinfo = now.to_c_tm();
      // Date: Day of week
      // const char *weekday[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
      const char *weekday[] = {"Dom","Lun","Mar","Mie","Jue","Vie","Sab"};
      const char *wday = weekday[timeinfo.tm_wday];
      // Date: month - day
      char dateStr[12];
      strftime(dateStr, sizeof(dateStr), "%d %b", &timeinfo);  // e.g. Jun 15
      // Day of the week + date below the time
      it.printf(60, 22, id(font_topbar), TextAlign::CENTER, "%s, %s", wday, dateStr);
      it.image(120, 10, id(icon_weather));
      it.printf(170, 22, id(font_topbar), TextAlign::CENTER, "%.0f°C", id(sensor_temperature).state);
      // SENSOR AND BATTERY LEVEL
      it.image(585, 10, id(icon_temperature));
      it.printf(610, 8, id(font_topbar), "%.0f°C", id(temp_sensor).state);
      it.image(658, 10, id(icon_humidity));
      it.printf(683, 8, id(font_topbar), "%.0f%%", id(hum_sensor).state);
      it.image(725, 10, id(icon_battery));
      it.printf(750, 8, id(font_topbar), "%.0f%%", id(battery_level).state);
      // QUOTE
      it.printf(10, 60, id(font_quote), "Si puedes imaginarlo, puedes crearlo");
      // CALENDAR
      it.image(10, 210, id(icon_calendar));
      // Función para limpiar caracteres especiales
      std::string clean_string = id(calendar_event_title).state;
      std::string cleaned = "";
      for (char c : clean_string) {
          unsigned char uc = c;
          if ((uc >= 32 && uc <= 126) || // ASCII imprimibles básicos
              uc == 225 || // á
              uc == 233 || // é
              uc == 237 || // í
              uc == 243 || // ó
              uc == 250 || // ú (ASCII extendido)
              uc == 195 || // Primer byte UTF-8 para acentos
              uc == 161 || uc == 169 || uc == 173 || uc == 179 || uc == 186) { // Segundos bytes UTF-8
              cleaned += c;
          }
      }
      it.printf(70, 208, id(font_event), "%s", cleaned.c_str());
      if (id(calendar_event_time).state.length() >= 16) {
          std::string date = id(calendar_event_time).state.substr(8, 2);  // Extrae el día (DD)
          std::string month = id(calendar_event_time).state.substr(5, 2); // Extrae el mes (MM)
          std::string hour = id(calendar_event_time).state.substr(11, 2); // Extrae la hora (HH)
          std::string minute = id(calendar_event_time).state.substr(14, 2); // Extrae los minutos (MM)
          
          // Mapea el número del mes a su nombre en español
          std::map<std::string, std::string> month_names = {
              {"01", "enero"}, {"02", "febrero"}, {"03", "marzo"}, {"04", "abril"},
              {"05", "mayo"}, {"06", "junio"}, {"07", "julio"}, {"08", "agosto"},
              {"09", "septiembre"}, {"10", "octubre"}, {"11", "noviembre"}, {"12", "diciembre"}
          };
          
          std::string month_name = month_names[month]; // Obtiene el nombre del mes
          
          // Imprime el evento y la fecha formateada
          it.printf(75, 233, id(font_start_time), "%s de %s, a las %s:%sh", date.c_str(), month_name.c_str(), hour.c_str(), minute.c_str());
      }
      // SESSION
      it.image(13, 290, id(icon_session));
      // Función para limpiar caracteres especiales
      std::string clean_string_s = id(calendar_session_title).state;
      std::string cleaned_s = "";
      for (char c : clean_string_s) {
          unsigned char uc = c;
          if ((uc >= 32 && uc <= 126) || // ASCII imprimibles básicos
              uc == 225 || // á
              uc == 233 || // é
              uc == 237 || // í
              uc == 243 || // ó
              uc == 250 || // ú (ASCII extendido)
              uc == 195 || // Primer byte UTF-8 para acentos
              uc == 161 || uc == 169 || uc == 173 || uc == 179 || uc == 186) { // Segundos bytes UTF-8
              cleaned_s += c;
          }
      }
      it.printf(72, 288, id(font_event), "%s", cleaned_s.c_str());
      if (id(calendar_session_time).state.length() >= 16) {
          std::string date = id(calendar_session_time).state.substr(8, 2);  // Extrae el día (DD)
          std::string month = id(calendar_session_time).state.substr(5, 2); // Extrae el mes (MM)
          std::string hour = id(calendar_session_time).state.substr(11, 2); // Extrae la hora (HH)
          std::string minute = id(calendar_session_time).state.substr(14, 2); // Extrae los minutos (MM)
          
          // Mapea el número del mes a su nombre en español
          std::map<std::string, std::string> month_names = {
              {"01", "enero"}, {"02", "febrero"}, {"03", "marzo"}, {"04", "abril"},
              {"05", "mayo"}, {"06", "junio"}, {"07", "julio"}, {"08", "agosto"},
              {"09", "septiembre"}, {"10", "octubre"}, {"11", "noviembre"}, {"12", "diciembre"}
          };
          
          std::string month_name = month_names[month]; // Obtiene el nombre del mes
          
          // Imprime el evento y la fecha formateada
          it.printf(75, 313, id(font_start_time), "%s de %s, a las %s:%sh", date.c_str(), month_name.c_str(), hour.c_str(), minute.c_str());
      }
      // SOCIAL COUNTER
      it.image(10, 440, id(icon_youtube));
      it.printf(52, 442, id(font_topbar), "%.0f Suscriptores", id(counter_youtube_subscribers).state);
      it.image(220, 445, id(icon_telegram));
      it.printf(252, 442, id(font_topbar), "%.0f Miembros", id(counter_telegram_members).state);
      // IMAGE
      it.image(360, 30, id(wallpaper));

Mapa estelar inteligente

Y si has visto este video y quieres convertirlo en un mapa estelar inteligente, a continuación te dejo todo el código de ESPHome.

substitutions:
# Device customization
# Personalización del dispositivo
  name: mapa-estelar
  friendly_name: Mapa Estelar
  star_chart: images/star_chart.png
  quote: 'Así empezó todo'
  event: '25 de Marzo de 2021 · Madrid'
  event_year: 2021
  event_month: 3
  event_day: 25
  calendar: calendar.aguacatec
  
# Sleep mode configuration
# Configuración del sleep mode
  run_duration: 60s 
  sleep_duration: 43200s # 12 Hours / Horas
# Other settings
# Otros ajustes
  allowed_characters: " ·¿?¡!#%'()+,-./:µ³°0123456789ABCDEFGHIJKLMNOPQRSTUVWYZabcdefghijklmnñopqrstuvwxyzáéíóú"
################################################################################################################
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: 600
    then:
      - delay: 2000ms
      - output.turn_on: bsp_battery_enable
      - delay: 200ms
      - component.update: battery_voltage
      - component.update: battery_level
      
esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
  encryption:
    key: "NheSBtt07y2sqb8ir47krVoRZKTbPyNxtO6KCm0a2bs="
ota:
  - platform: esphome
    password: "18f3570ad68c9140294940c9a395f65e"
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Mapa-Estelar Fallback Hotspot"
    password: "x57ndOAjd7d5"
captive_portal:
deep_sleep:
  id: deep_sleep_1
  run_duration: ${run_duration} 
  sleep_duration: ${sleep_duration}  
  wakeup_pin: GPIO3
  wakeup_pin_mode: INVERT_WAKEUP
font:
  - file: "gfonts://Lobster"
    id: font_quote
    size: 40
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_event
    size: 16
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_count
    size: 12
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_current_date
    size: 25
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_calendar_event
    size: 20
    glyphs: ${allowed_characters}
  - file: "gfonts://Inter@700"
    id: font_calendar_time
    size: 16
    glyphs: ${allowed_characters}
  - file: "gfonts://Kanit"
    id: font_battery_level
    size: 20
    glyphs: ${allowed_characters}
i2c:
  scl: GPIO20
  sda: GPIO19
image:
# Star chart
# Mapa estelar
  - file: ${star_chart} 
    id: star_chart
    resize: 400x400
    type: BINARY  
    dither: FLOYDSTEINBERG
    transparency: chroma_key
    invert_alpha: false
  - file: mdi:battery-70
    id: icon_battery
    resize: 25x25
    type: BINARY
    transparency: chroma_key
output:
  - platform: gpio
    pin: GPIO21
    id: bsp_battery_enable
sensor:
  - platform: adc
    pin: GPIO1
    name: "Battery Voltage"
    id: battery_voltage
    internal: true
    attenuation: 12db
    filters:
      - multiply: 2.0  # Voltage divider compensation
  - platform: template
    name: "Battery Level"
    id: battery_level
    unit_of_measurement: "%"
    icon: "mdi:battery"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    filters:
      - calibrate_linear:
          - 4.15 -> 100.0
          - 3.96 -> 90.0
          - 3.91 -> 80.0
          - 3.85 -> 70.0
          - 3.80 -> 60.0
          - 3.75 -> 50.0
          - 3.68 -> 40.0
          - 3.58 -> 30.0
          - 3.49 -> 20.0
          - 3.41 -> 10.0
          - 3.30 -> 5.0
          - 3.27 -> 0.0
      - clamp:
          min_value: 0
          max_value: 100
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
text_sensor:
  - platform: homeassistant
    id: calendar_event_title
    entity_id: ${calendar}
    attribute: message
    internal: true
  - platform: homeassistant
    id: calendar_event_time
    entity_id: ${calendar}
    attribute: start_time
    internal: true
time:
  - platform: homeassistant
    id: ha_time
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: 30s
    rotation: 270
    lambda: |-
      // Center
      int x_center = it.get_width() / 2;
      int y_center = it.get_height() / 2;
      // Background
      it.filled_rectangle(0, 0, it.get_width(), it.get_height(), COLOR_ON);
      // Start chart
      int img_x = (it.get_width() - 400) / 2;      
      it.image(img_x, 30, id(star_chart), COLOR_OFF, COLOR_ON);
      // Quote
      it.printf(x_center, 450, id(font_quote), COLOR_OFF, TextAlign::CENTER, "${quote}");
      // Separator
      int line_width = it.get_width() * 0.7;
      int line_x_start = (it.get_width() - line_width) / 2;
      it.filled_rectangle(line_x_start, 490, line_width, 2, COLOR_OFF);
      // Event data
      it.printf(x_center, 520, id(font_event), COLOR_OFF, TextAlign::CENTER, "${event}");
      // Counter
      auto now = id(ha_time).utcnow();
      int now_year = now.year;
      int now_month = now.month;
      int now_day = now.day_of_month;
      
      int ref_year = ${event_year};
      int ref_month = ${event_month};
      int ref_day = ${event_day};
      
      int years = now_year - ref_year;
      int months = now_month - ref_month;
      int days = now_day - ref_day;
      
      if (days < 0) {
        months--;
        days += 30;
      }
      if (months < 0) {
        years--;
        months += 12;
      }
      
      char time_passed[64];
      snprintf(time_passed, sizeof(time_passed), 
              "Hemos pasado %d años, %d meses y %d días juntos", 
              years, months, days);
      
      it.printf(x_center, 545, id(font_count), COLOR_OFF, TextAlign::CENTER, "%s", time_passed);
      // Separator
      it.filled_rectangle(line_x_start, 570, line_width, 2, COLOR_OFF);
      // Current date
      auto now2 = id(ha_time).now();
      struct tm timeinfo = now2.to_c_tm();
      const char *weekday[] = {"Dom","Lun","Mar","Mie","Jue","Vie","Sab"};
      const char *wday = weekday[timeinfo.tm_wday];
        const char *months_es[] = {"Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"};
        const char *month_name = months_es[timeinfo.tm_mon];
      char year_short[4];
      snprintf(year_short, sizeof(year_short), "'%02d", timeinfo.tm_year % 100);
      it.printf(x_center, 620, id(font_current_date), COLOR_OFF, TextAlign::CENTER, 
                "%s, %d %s %s", wday, timeinfo.tm_mday, month_name, year_short);
      // Calendar
      it.printf(x_center, 670, id(font_calendar_event), COLOR_OFF, TextAlign::CENTER, 
                "%s", id(calendar_event_title).state.c_str());
      if (id(calendar_event_time).state.length() >= 16) {
          std::string date = id(calendar_event_time).state.substr(8, 2);  // Extrae el día (DD)
          std::string month = id(calendar_event_time).state.substr(5, 2); // Extrae el mes (MM)
          std::string hour = id(calendar_event_time).state.substr(11, 2); // Extrae la hora (HH)
          std::string minute = id(calendar_event_time).state.substr(14, 2); // Extrae los minutos (MM)
          
          // Mapea el número del mes a su nombre en español
          std::map<std::string, std::string> month_names = {
              {"01", "enero"}, {"02", "febrero"}, {"03", "marzo"}, {"04", "abril"},
              {"05", "mayo"}, {"06", "junio"}, {"07", "julio"}, {"08", "agosto"},
              {"09", "septiembre"}, {"10", "octubre"}, {"11", "noviembre"}, {"12", "diciembre"}
          };
          
          std::string month_name = month_names[month]; // Obtiene el nombre del mes
          
          // Imprime el evento y la fecha formateada
          it.printf(x_center, 700, id(font_calendar_time), COLOR_OFF, TextAlign::CENTER, "%s de %s, a las %s:%sh", date.c_str(), month_name.c_str(), hour.c_str(), minute.c_str());
      }
      // Battery level
      int bat_x = it.get_width() - 70;  // 430px (480-50)
      int bat_y = it.get_height() - 40; // 760px (800-40)
      it.image(bat_x, bat_y, id(icon_battery), COLOR_OFF);
      it.printf(bat_x + 25, bat_y, id(font_battery_level), COLOR_OFF, 
                "%.0f%%", id(battery_level).state);