Integrar Lilygo T-Display S3 Pro en HA

Integrar Lilygo T-Display S3 Pro en HA

Hoy te enseño a integrar Lilygo T-Display S3 Pro en HA, un dispositivo con el que he dado vida al «Habbit Remote».

Lilygo T-Display S3 Pro

Con la marca Lilygo tengo una de esas relaciones «amor-odio». Por un lado, me encantan muchos de sus dispositivos (mi favorito, el Lilygo T-Display S3 Long). Por otro, ¿sabes esas marcas que tienen un montón de documentación que nos ayuda a integrar sus dispositivos? Pues Lilygo no es una de ellas.

Pero bueno, «lloriqueos» aparte, hoy te quiero enseñar el Lilygo T-Display S3 Pro. En este caso se trata de un pequeño dispositivo (72*32*18mm), que tiene una pantalla táctil de 2.33 pulgadas (222×480 píxeles) compatible con LVGL, montada sobre una placa ESP32-S3R8 con 8 MB de PSRAM.

Pero lo que, a mi parecer, le hace diferente a otros productos es que incorpora una batería de 470 mAh, un par de botones físicos y (dependiendo del modelo que escojas) una cámara en su parte posterior.

Habbit Remote

Con las características que te acabo de contar y el pequeño tamaño del dispositivo, me parece que reúne todo lo necesario para ser un pequeño mando a distancia que te permita controlar cualquier cosa de Home Assistant.

💡 Si, ya sé que existen mandos a distancia compatibles con Home Assistant, pero todos me parecen caros, enormes y poco personalizables.

La idea surge después de que mi Habbit Desk se haya convertido en uno de los dispositivos más útiles, que uso cada día continuamente mientras estoy en mi escritorio. Por ese motivo lo he bautizado como el «Habbit Remote».

Siguiendo el mismo concepto y adaptando su interfaz, he conseguido crear un dispositivo que es perfecto para tenerlo en cualquier habitación con muchos dispositivos integrados. Además, como puedes apagarlo con un interruptor cuando no lo estás usando, puedes alargar mucho la duración de la batería.

Requisitos previos

Para integrar Lilygo T-Display S3 Pro en HA y montar el Habbit Remote necesitas:

  • Haber instalado ESPHome en Home Assistant y tener conocimientos mínimos sobre cómo funciona.
  • El Lilygo T-Display S3 Pro. Recuerdan que lo venden con y sin cámara, aunque para este proyecto te valdrá el que no tiene cámara.
  • Un cable USB-C para alimentar la placa de DATOS (con un cable de carga no vas a poder instalar el software).

LILYGO ® Módulo de placa de desarrollo LCD táctil T-Display-S3-Pro ESP32-S3 de 2,33 pulgadas
Aliexpress
51,88€
LILYGO ® Módulo de placa de desarrollo LCD táctil T-Display-S3-Pro ESP32-S3 de 2,33 pulgadas
LILYGO ® T-Display-S3-Pro ESP32-S3 pantalla táctil Placa de desarrollo LCD de 2,33 pulgadas módulo de Sensor de luz Digital I2C WiFi Bluetooth
Aliexpress
58,36€
LILYGO ® T-Display-S3-Pro ESP32-S3 pantalla táctil Placa de desarrollo LCD de 2,33 pulgadas módulo de Sensor de luz Digital I2C WiFi Bluetooth
Nuevo en STOCK LILYGO T-Display-S3-Pro Placa de desarrollo de ESP32-S3 con pantalla táctil ST7796 de 2,33 pulgadas I2C Sensor de luz Digital WiFi M
Aliexpress
59,79€
Nuevo en STOCK LILYGO T-Display-S3-Pro Placa de desarrollo de ESP32-S3 con pantalla táctil ST7796 de 2,33 pulgadas I2C Sensor de luz Digital WiFi M
LILYGO® T-Display-S3-Pro Placa de desarrollo LCD con pantalla táctil de, módulo de Sensor de luz Digital I2C de 2,33 pulgadas, WiFi, Bluetooth
Aliexpress
39,58€
LILYGO® T-Display-S3-Pro Placa de desarrollo LCD con pantalla táctil de, módulo de Sensor de luz Digital I2C de 2,33 pulgadas, WiFi, Bluetooth
*Algún precio puede haber cambiado desde la última revisión

Configuración de ESPHome

Para integrar Lilygo T-Display S3 Pro en HA y flashearlo con el código del Habbit Remote 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, “Habbit Remote”).
  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 el enlace «Edit» del bloque correspondiente al dispositivo que acabas de crear. Copia este código y consérvalo, ya que utilizaremos algún fragmento.
  5. Reemplázalo por este código, haciendo las siguientes adaptaciones:
    • Ajusta el ‘name’ y ‘friendly-name’ como el nombre que le quieras dar a tu asistente.
    • Añade la información que te había generado el complemento en el apartado ‘api’ (‘encryption key’). Haz lo mismo para el campo ‘password’ bajo el apartado ‘ota’, y para los campos ‘ssid’ y ‘password’ bajo el apartado ‘wifi’ y ‘ap’.
    • De momento es suficiente, ya que prefiero que empieces a personalizarlo tras explicarte cómo funciona cada apartado.
 substitutions:

# Device customization
# Personalización del dispositivo

  name: habbit_remote
  friendly_name: Habbit Remote
  logo: "https://aguacatec.es/wp-content/uploads/2025/11/habbit_aguacatec.png"
  background_color: 0x000000
  text_color: 0xFFFFFF
  text_color_secondary: 0x626262

  iddle_time: 20s

  greeting1: "Habbit Remote"
  greeting2: "by aguacatec.es"

# Entities
# Entidades

  weather_temperature: sensor.openweathermap_temperature
  home_temperature: sensor.home_temperatura
  home_humidity: sensor.home_humedad

  climate: climate.salon
  cover: cover.salon
  light_ceiling: switch.zbminil2
  light_ambient: light.tira_led_sofa
  vacuum: vacuum.roborock_qr_598
  tv: media_player.smart_tv

# Other settings
# Otros ajustes

  allowed_characters: " ¿?¡!#%'()+,-./:µ³°0123456789ABCDEFGHIJKLMNOPQRSTUVWYZabcdefghijklmnopqrstuvwxyzáéíóú"

################################################################################################################

################################################################################################################

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  platformio_options:
    build_flags: 
      - '-DBOARD_HAS_PSRAM'
      - '-DUSING_DISPLAY_PRO_V1'
      - '-DARDUINO_ESP32S3_DEV'
      - '-DARDUINO_RUNNING_CORE=1'
      - '-DARDUINO_EVENT_RUNNING_CORE=1'
    board_build.flash_mode: qio
    board_build.f_flash: '80000000L'
    board_build.arduino.memory_type: 'qio_opi'
    board_upload.maximum_size: '16777216'
    board_upload.maximum_ram_size: '327680'
    board_upload.speed: '921600'
  on_boot:
    priority: -200   
    then:
      - delay: 3s
      - component.update: battery
      
esp32:
  board: esp32s3box
  framework:
    type: arduino
  flash_size: 16MB       

psram:
  mode: octal
  speed: 80MHz

# Enable logging
logger:

# Enable Home Assistant API

api:
  encryption:
    key: "Df4VasddsaoqfLSaq7SOxmSQ234rfeq2"

ota:
  - platform: esphome
    password: "ewf23F004acb632RFDWQFE23"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Remote Fallback Hotspot"
    password: "F32W3c6cd34"

captive_portal:

binary_sensor:                                                             
  - platform: gpio
    pin: 
      number: 12
      inverted: true
    name: "Button UP"
    id: button1
    icon: mdi:arrow-up-bold
    on_press:
      then:
        - if:
            condition: lvgl.is_paused
            then:
              - light.turn_on: backlight
              - lvgl.resume:
              - lvgl.page.show: page_home
            else:
              - if:
                  condition:
                    lvgl.page.is_showing: page_cover
                  then:
                    - homeassistant.action:
                        action: cover.set_cover_position
                        data:
                          entity_id: ${cover}
                          position: !lambda |-
                            int current = (int) id(cover_position_value).state;
                            return min(100, current + 10);
                  else:
                    - if:
                        condition:
                          lvgl.page.is_showing: page_devices
                        then:
                          - lvgl.page.show: page_devices2
                        else:
                          - lvgl.page.show: page_devices

  - platform: gpio
    pin:
      number: 16
      inverted: true
    name: "Button DOWN"
    id: button2
    icon: mdi:arrow-down-bold
    on_press:
      then:
        - if:
            condition: lvgl.is_paused
            then:
              - light.turn_on: backlight
              - lvgl.resume:
              - lvgl.page.show: page_home
            else:
              - if:
                  condition:
                    lvgl.page.is_showing: page_cover
                  then:
                    - homeassistant.action:
                        action: cover.set_cover_position
                        data:
                          entity_id: ${cover}
                          position: !lambda |-
                            int current = (int) id(cover_position_value).state;
                            return max(0, current - 10);
                  else:
                    - if:
                        condition:
                          lvgl.page.is_showing: page_devices
                        then:
                          - lvgl.page.show: page_devices2
                        else:
                          - lvgl.page.show: page_devices

  - platform: gpio
    pin: 
      number: 21
      inverted: true
    name: "Screen Button"
    id: button_screen
    icon: mdi:circle-outline
    filters:
      - delayed_off: 1s
    on_press:
      then:
        - if:
            condition: lvgl.is_paused
            then:
              - light.turn_on: backlight
              - lvgl.resume:
              - lvgl.page.show: page_home

  - platform: gpio
    pin: 
      number: 0
      inverted: true
    internal: True
    name: "Boot Button"
    id: button_b

  - platform: sy6970
    sy6970_id: pmu
    vbus_connected:
      name: "USB Connected"
      id: usb_connected
      internal: True
    charging:
      name: "Charging"
      id: is_charging
    charge_done:
      name: "Charge Done"
      internal: True

font:
  - file: "gfonts://Kanit"
    id: font_clock
    size: 35
    glyphs: ${allowed_characters}
  - file: "gfonts://Kanit"
    id: font_date
    size: 20
    glyphs: ${allowed_characters}
  - file: "gfonts://Kanit"
    id: font_greeting
    size: 18
    glyphs: ${allowed_characters}
  - file: "gfonts://Material+Symbols+Outlined"
    id: font_status_icons
    size: 25
    glyphs: [
      "\U0000e430", # sun
      "\U0000ebe2", # battery
      " ",
    ]
  - file: "gfonts://Material+Symbols+Outlined"
    id: font_home_icons
    size: 30
    glyphs: [
      "\U0000e430", # sun
      "\U0000e1ff", # thermometer
      "\U0000f87e", # water-percent
      "\U0000ebe2", # battery
      "\U0000e286", # blinds-shade
      "\U0000ec1f", # blinds-shade-close
      " ",
    ]
  - file: "gfonts://Kanit"
    id: font_home_info
    size: 25
    glyphs: ${allowed_characters}
  - file: "gfonts://Material+Symbols+Outlined"
    id: font_device_icons
    size: 100
    glyphs: [
      "\U0000f02a", # ceiling-light
      "\U0000f168", # fan
      "\U0000e286", # blinds-shade
      "\U0000ec1f", # blinds-shade-close
      "\U0000ef55", # fire
      "\U0000efc5", # vacuum
      "\U0000e333", # tv
      "\U0000ea28", # videogames
      "\U0000e1c4", # play
      "\U0000e1a2", # pause
      "\U0000eaaa", # base
      "\U0000eee1", # locate
      "\U0000f418", # power
      "\U0000ef71", # stop
      "\U0000ebfe", # spotlight
      " ",
    ]
  - file: "gfonts://Kanit"
    id: font_slider_value
    size: 60
    glyphs: ${allowed_characters}

i2c:
  - id: bus_a
    sda: 5
    scl: 6

image:
  - file: ${logo}
    id: logo
    resize: 180x180
    type: RGB565
    transparency: alpha_channel

  - file: mdi:robot-vacuum
    id: icon_vacuum
    resize: 100x100
    type: BINARY
    transparency: chroma_key
  - file: mdi:led-strip-variant
    id: icon_ledstrip
    resize: 100x100
    type: BINARY
    transparency: chroma_key

interval:
  - interval: 60s
    then:
      - lvgl.label.update:
          id: lbl_date
          text: !lambda 'return id(esptime).now().strftime("%d/%m/%y");'
      - lvgl.label.update:
          id: lbl_clock
          text: !lambda 'return id(esptime).now().strftime("%H:%M");'

light:
  - platform: monochromatic
    output: gpio_48_backlight_pwm
    name: "Display"
    icon: mdi:cellphone
    id: backlight
    restore_mode: ALWAYS_ON
    on_turn_on:
      - light.turn_on:
          id: backlight
          brightness: 100%

output:
  - platform: ledc
    pin: 48
    id: gpio_48_backlight_pwm
    channel: 1
    frequency: 20000

sensor:

  - platform: sy6970
    sy6970_id: pmu
    battery_voltage:
      name: "Battery Voltage"
      id: battery_voltage
      internal: True
    charge_current:
      name: "Battery Current"
      internal: True
    vbus_voltage:
      name: "VBUS Voltage"
      internal: True
  - platform: template
    name: "Battery"
    id: battery
    unit_of_measurement: "%"
    device_class: battery
    accuracy_decimals: 0
    lambda: |-
      float v = id(battery_voltage).state;
      if (v >= 4.1) return 100.0;
      if (v <= 3.3) return 0.0;
      return (v - 3.3) / (4.1 - 3.3) * 100.0;
    update_interval: 60s
    on_value:
      then:
      - lvgl.label.update:
          id: lbl_battery_percentage
          text:
            format: "%.0f%%"
            args:
              - id(battery).state

# Home Assistant sensors
# Sensores de Home Assistant

  - platform: homeassistant
    id: home_icon_weather_temperature
    entity_id: ${weather_temperature}
    internal: true
    on_value:
      then:
      - lvgl.label.update:
          id: lbl_weather_temperature
          text:
            format: "%.0f °C"
            args:
              - id(home_icon_weather_temperature).state
  - platform: homeassistant
    id: home_icon_home_temperature
    entity_id: ${home_temperature}
    internal: true
    on_value:
      then:
      - lvgl.label.update:
          id: lbl_home_temperature
          text:
            format: "%.0f °C"
            args:
              - id(home_icon_home_temperature).state
  - platform: homeassistant
    id: home_icon_home_humidity
    entity_id: ${home_humidity}
    internal: true
    on_value:
      then:
      - lvgl.label.update:
          id: lbl_home_humidity
          text:
            format: "%.0f%%"
            args:
              - id(home_icon_home_humidity).state

  - platform: homeassistant
    id: cover_position_value
    entity_id: ${cover}
    attribute: current_position
    internal: true
    filters:
      - lambda: |-
          if (isnan(x)) { return 0; }
          else { return x; }
    on_value:
      then:
        - lvgl.slider.update:
            id: slider_cover
            value: !lambda 'return (int)x;'
        - lambda: |-
            int position = (int) id(cover_position_value).state;
            char buf[8];
            snprintf(buf, sizeof(buf), "%d%%", position);
            // Actualizar texto del label LVGL
            lv_label_set_text(id(slider_cover_value), buf);

spi:
  mosi_pin: 17
  clk_pin: 18

sy6970:
  id: pmu
  address: 0x6A
  i2c_id: bus_a
  enable_adc: true
  charge_enabled: true
  input_current_limit: 1500   # mA
  charge_voltage: 4208        # mV
  charge_current: 512         # mA
  update_interval: 30s

text_sensor:
  - platform: sy6970
    sy6970_id: pmu
    charge_status:
      name: "Charge Status"
      internal: True
    ntc_status:
      name: "Battery Temp Status"
      internal: True

# Home Assistant sensors
# Sensores de Home Assistant

  - platform: homeassistant
    id: device_light_ceiling
    entity_id: ${light_ceiling}
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: lbl_device_light_ceiling
            text_color: !lambda |-
              if (x == "on") {
                return lv_color_hex(0xe6d754);  
              } else {
                return lv_color_hex(0x626262);  
              }
  - platform: homeassistant
    id: device_light_ambient
    entity_id: ${light_ambient}
    internal: true
    on_value:
      then:
        - lvgl.image.update:
            id: lbl_device_light_ambient
            image_recolor: !lambda |-
              if (x == "on") {
                return lv_color_hex(0xe6d754);  
              } else {
                return lv_color_hex(0x626262);  
              }
  - platform: homeassistant
    id: device_cover
    entity_id: ${cover}
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: lbl_device_cover
            text: !lambda |-
              if (x == "open") {
                return "\U0000e286";
              } else {
                return "\U0000ec1f";
              }
            text_color: !lambda |-
              if (x == "open") {
                return lv_color_hex(0xffffff);  
              } else {
                return lv_color_hex(0x626262);  
              }

  - platform: homeassistant
    id: device_climate
    entity_id: ${climate}
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: lbl_device_climate
            text_color: !lambda |-
              if (x == "heat") {
                return lv_color_hex(0xffae00);  
              } else {
                return lv_color_hex(0x626262);  
              }

  - platform: homeassistant
    id: device_vacuum
    entity_id: ${vacuum}
    internal: true
    on_value:
      then:
        - lvgl.image.update:
            id: lbl_device_vacuum
            image_recolor: !lambda |-
              if (x == "docked") {
                return lv_color_hex(0x626262);  
              } else {
                return lv_color_hex(0xffffff);  
              }

  - platform: homeassistant
    id: device_tv
    entity_id: ${tv}
    internal: true
    on_value:
      then:
        - lvgl.label.update:
            id: lbl_device_tv
            text_color: !lambda |-
              if (x == "on") {
                return lv_color_hex(0xe6d754);  
              } else {
                return lv_color_hex(0x626262);  
              }

time:
  - platform: homeassistant
    id: esptime
    on_time_sync:
      then:
        - lvgl.label.update:
            id: lbl_clock
            text: !lambda 'return id(esptime).now().strftime("%H:%M");'
        - lvgl.label.update:
            id: lbl_date
            text: !lambda 'return id(esptime).now().strftime("%d/%m/%y");'

touchscreen:
  - platform: cst226
    id: main_touch
    reset_pin: 13 
    on_update:
    - lambda: |-
          for (auto touch: touches)  {
              if (touch.state <= 2) {
                 ESP_LOGI("Touch points:", "id=%d x=%d, y=%d", touch.id, touch.x, touch.y);
              }
          }
                                    #define BOARD_SENSOR_IRQ    21
                                    #define BOARD_TOUCH_RST     13
                                    #define BOARD_TOUCH_IRQ     7

display:
  - platform: ili9xxx
    id: main_display
    model: ST7796
    dimensions:
      height: 480
      width: 222
      offset_height: 0
      offset_width: 49
    color_order: bgr    
    data_rate: 80MHz
    cs_pin: 39 
    dc_pin: 9
    reset_pin: 47
    invert_colors: true

lvgl:
  displays:
    - main_display
  touchscreens:
    - main_touch
  buffer_size: 25%
  on_idle:
    - timeout: ${iddle_time}
      then:
        - lvgl.pause:
        - light.turn_off:
            id: backlight
  style_definitions:
    - id: button_device_1
      x: 10
      width: 130
      height: 130
      pad_all: 0
      translate_y: 0
      bg_color: 0x000000
      border_color: 0x000000
      align: TOP_MID
    - id: button_device_2
      x: 10
      width: 130
      height: 130
      pad_all: 0
      translate_y: 110
      bg_color: 0x000000
      border_color: 0x000000
      align: TOP_MID
    - id: button_device_3
      x: 10
      width: 130
      height: 130
      pad_all: 0
      translate_y: 230
      bg_color: 0x000000
      border_color: 0x000000
      align: TOP_MID
    - id: button_device_4
      x: 10
      width: 130
      height: 130
      pad_all: 0
      translate_y: 350
      bg_color: 0x000000
      border_color: 0x000000
      align: TOP_MID

    - id: slider_front_cover
      width: 166
      height: 400
      bg_color: 0x9c9c79
      radius: 10
      align: CENTER
    - id: slider_back_cover
      width: 166
      height: 400
      bg_color: 0xb4b4a9
      radius: 10
      align: CENTER
    - id: slider_label_values_cover
      align: CENTER
      text_font: font_slider_value
      text_color: 0xffffff

  pages:

    - id: page_home
      bg_color: ${background_color}
      widgets:

      - image:
          align: CENTER
          src: logo
          pad_top: 40
          on_click:
            then:
              - lvgl.page.show: page_devices

      - label:
          align: TOP_LEFT
          id: lbl_clock
          text: "00:00"
          text_color: ${text_color}
          text_font: font_clock
          pad_left: 5
      - label:
          align: TOP_LEFT
          id: lbl_date
          text: "00/00/00"
          text_color: ${text_color_secondary}
          text_font: font_date
          pad_top: 45
          pad_left: 5

      - label:
          align: TOP_RIGHT
          text_font: font_status_icons
          text_color: ${text_color_secondary}
          text: "\U0000ebe2" 
          pad_right: 7
          pad_top: 10
      - label:
          id: lbl_battery_percentage
          align: TOP_RIGHT
          text_font: font_home_info
          text_color: ${text_color_secondary}
          text: "99%"  
          pad_right: 30
          pad_top: 5

      - label:
          align: TOP_MID
          id: lbl_greeting1
          text: "${greeting1}"
          text_color: ${text_color}
          text_font: font_greeting
          pad_top: 110
      - label:
          align: TOP_MID
          id: lbl_greeting2
          text: "${greeting2}"
          text_color: ${text_color_secondary}
          text_font: font_greeting
          pad_top: 130

      - label:
          align: CENTER
          text_font: font_home_icons
          text_color: 0xe6d754
          text: "\U0000e430"  
          pad_right: 50
          pad_top: 300
      - label:
          id: lbl_weather_temperature
          align: CENTER
          text_font: font_home_info
          text_color: ${text_color}
          text: "20 °C"  
          pad_left: 50
          pad_top: 300

      - label:
          align: CENTER
          text_font: font_home_icons
          text_color: 0xe65476
          text: "\U0000e1ff"  
          pad_top: 380
          pad_right: 165
      - label:
          id: lbl_home_temperature
          align: CENTER
          text_font: font_home_info
          text_color: ${text_color}
          text: "20 °C"  
          pad_top: 380
          pad_right: 70

      - label:
          align: CENTER
          text_font: font_home_icons
          text_color: 0x54c9e6
          text: "\U0000f87e"  
          pad_top: 380
          pad_left: 45
      - label:
          id: lbl_home_humidity
          align: CENTER
          text_font: font_home_info
          text_color: ${text_color}
          text: "50%"  
          pad_top: 380
          pad_left: 130

    - id: page_devices
      bg_color: ${background_color}
      widgets:

      - obj:
          styles: button_device_1
          widgets:
          - label:
              id: lbl_device_light_ceiling
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000f02a"  
          on_press:
            then:
              - homeassistant.action:
                  service: switch.toggle
                  data:
                    entity_id: ${light_ceiling}
      - obj:
          styles: button_device_2
          widgets:
          - image:
              id: lbl_device_light_ambient
              src: icon_ledstrip
              image_recolor: ${text_color_secondary}
              image_recolor_opa: COVER
              pad_top: 12
          on_press:
            then:
              - homeassistant.action:
                  service: light.toggle
                  data:
                    entity_id: ${light_ambient}

      - obj:
          styles: button_device_3
          widgets:
          - label:
              id: lbl_device_cover
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000e286"  
          on_press:
            then:
              lvgl.page.show: page_cover

      - obj:
          styles: button_device_4
          widgets:
          - label:
              id: lbl_device_climate
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000ef55"  
          on_press:
            then:
              - homeassistant.action:
                  service: climate.toggle
                  data:
                    entity_id: ${climate}

    - id: page_devices2
      bg_color: ${background_color}
      widgets:

      - obj:
          styles: button_device_1
          widgets:
          - image:
              id: lbl_device_vacuum
              src: icon_vacuum
              image_recolor: ${text_color_secondary}
              image_recolor_opa: COVER
          on_press:
            then:
              lvgl.page.show: page_vacuum

      - obj:
          styles: button_device_2
          widgets:
          - label:
              id: lbl_device_tv
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000e333"  
          on_press:
            then:
              - homeassistant.action:
                  service: media_player.turn_off
                  data:
                    entity_id: ${tv}

      - obj:
          styles: button_device_3
          widgets:
          - label:
              id: lbl_device_fan
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000f168"  

      - obj:
          styles: button_device_4
          widgets:
          - label:
              id: lbl_device_videogames
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000ea28"  


    - id: page_cover
      bg_color: ${background_color}
      widgets:
      - slider:
          styles: slider_back_cover
          id: slider_cover
          indicator:
            styles: slider_front_cover
          knob:
            opa: 0
          min_value: 0
          max_value: 100
          on_release:
            - homeassistant.action:
                action: cover.set_cover_position
                data:
                  entity_id: ${cover}
                  position: !lambda return int(x);
      - label:
          styles: slider_label_values_cover
          id: slider_cover_value
          text: "0%"
      - label:
          text_font: font_home_icons
          text_color: 0xFFFFFF
          text: "\U0000e286"  
          align: TOP_MID
          pad_top: 60
      - label:
          text_font: font_home_icons
          text_color: 0xFFFFFF
          text: "\U0000ec1f"  
          align: BOTTOM_MID
          pad_bottom: 60

    - id: page_vacuum
      bg_color: ${background_color}
      widgets:

      - obj:
          styles: button_device_1
          widgets:
          - label:
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000e1c4" # play
          on_press:
            then:
              - homeassistant.action:
                  service: vacuum.start
                  data:
                    entity_id: ${vacuum}
      - obj:
          styles: button_device_2
          widgets:
          - label:
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000e1a2" # pause
          on_press:
            then:
              - homeassistant.action:
                  service: vacuum.pause
                  data:
                    entity_id: ${vacuum}
      - obj:
          styles: button_device_3
          widgets:
          - label:
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000eaaa" # base
          on_press:
            then:
              - homeassistant.action:
                  service: vacuum.return_to_base
                  data:
                    entity_id: ${vacuum}
      - obj:
          styles: button_device_4
          widgets:
          - label:
              text_font: font_device_icons
              text_color: ${text_color_secondary}
              text: "\U0000eee1" # locate
          on_press:
            then:
              - homeassistant.action:
                  service: vacuum.locate
                  data:
                    entity_id: ${vacuum}
  1. Cuando hayas terminado de editar el código pulsa en “Save” e “Install”. Selecciona la opción “Manual download” y espera a que el código se compile.
  2. Cuando termine, selecciona la opción «Factory format» para descargar el fichero ‘.bin’ correspondiente.
  3. Ahora ve a la página de ESPHome y pulsa en “Connect”. En la ventana emergente selecciona tu placa y pulsa en “Conectar”.
  4. Ahora pulsa en “Install” y selecciona el fichero ‘.bin’ obtenido en el paso 7. De nuevo, pulsa en “Install”.
  5. 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’. Como siempre, te recomiendo que asignes una IP fija en tu router para evitar fallos en el futuro si esta cambia.
  6. Para terminar ve a Configuración > Dispositivos y servicios > ESPHome. Pulsa sobre el enlace «Configurar» correspondiente a tu dispositivo. En la ventana emergente marca la casilla «Permitir que el dispositivo realice acciones de Home Assistant» y pulsa en «Enviar». Esto va a permitir que podamos controlar nuestros dispositivos desde la pantalla.

Personalización del Habbit Remote

Vale, una vez que has terminado de integrar Lilygo T-Display S3 Pro en HA, déjame que te explique en detalle cómo funciona para que puedas personalizarlo a tu gusto.

💡 Usa estas instrucciones para replicar y adaptar la plantilla, o simplemente para observar como he construido ciertos bloques y crear tu propia plantilla.

Personalización rápida del dispositivo

En las primeras líneas del código encontrar distintos parámetros que puedes personalizar fácilmente, reemplazando su valor:

  • Name. Este es el nombre que el dispositivo adopta, dentro de ESPHome (no puede contener espacios, mayúsculas, ni caracteres especiales).
  • Friendly_name. Nombre «amistoso» del dispositivo, con el que aparecerá en Home Assistant.
  • Logo. Ruta (externa o local) a la imagen que quieres que aparezca en la pantalla de inicio, centrada.
  • Background_colorCódigo hexadecimal del color de fondo de la pantalla, precedido de un ‘0x’ (por ejemplo, ‘0x000000’).
  • Text_colorCódigo hexadecimal del texto e iconos principales (reloj, sensores, etc.).
  • Text_color_secondaryCódigo hexadecimal del texto e iconos secundarios (fecha, dispositivos inactivos, etc.).
  • Iddle_time. Tiempo de inactividad tras el cual la pantalla se apaga automáticamente.
  • Greeting 1. Texto que aparece en la pantalla de inicio, en la línea superior.
  • Greeting 2. Texto que aparece en la pantalla de inicio, en la línea inferior.

Entidades de Home Assistant

El siguiente apartado del código muestra cómo importamos información desde las entidades de Home Assistant a nuestro dispositivo. Esto sirve para mostrar su estado en la pantalla, o para controlarlos desde la misma.

Si quieres reutilizar la plantilla, sólo tienes que reemplazarar las entidades del ejemplo por las que corresponden a tu instancia. Ahora, si quieres modificar la interfaz te sugiero que eches un vistazo a cómo las utilizo:

  • Las entidades con valores numéricos (como la temperatura o la humedad), se encuentran dentro del componente ‘sensor’ del código.
  • Las entidades con valores textuales (como «encendido» o «limpiando»), se encuentran dentro del componente ‘text_sensor’ del código.

Control de dispositivos

Si ya conoces cómo utilizar LVGL con ESPHome, no te costará entender como funciona la interfaz mirando el código. Si no, quizás te ayude tener en cuenta cómo la he diseñado:

  • Cuando está inactivo, puedes encender la pantalla pulsando sobre ella o en cualquier de los dos botones físicos del lateral derecho.
  • Cuando está activo, pulsando en los dos botones físicos puedes cambiar entre las distintas páginas de dispositivos. Puedes añadir tantas páginas como necesites.
  • Pulsando en el icono de cada uno de los dispositivos, puedes alternar su estado (por ejemplo, encender/apagar una luz). Cuando cambia el estado, también cambia el color del icono para que lo identifiques a simple vista.
  • En algunos casos, al pulsar sobre el icono del dispositivo, puedes acceder a otra página con más controles. Por ejemplo, el icono de las persianas lleva a un ‘slider’ para controlar el porcentaje de apertura, y el del robot aspirador permite iniciar la limpieza, pausarla o mandarlo a la estación. Todo es 100% personalizable.
🛟 ¿Dudas? Si necesitas ayuda entra aquí 👈 🎁 Y si te ha gustado y quieres más... 🥑

¡Copiado!
20%
Descuento en todos los productos que funcionan con Curve
¡Copiado!
12%
Descuento en todos los productos de la tienda de Zemismart
¡Copiado!
10%
Descuento en todos los productos de la tienda de Shelly Spain
¡Copiado!
10%
Descuento en todos los productos probados por Aguacatec
¡Copiado!
10%
Descuento en todos los productos de Home Assistant
¡Copiado!
10%
Descuento en todos los productos de la tienda.
¡Copiado!
5%
Descuento en las pantallas de la serie CrowPanel
¡Copiado!
5%
Descuento en todos los productos de la tienda de Lilygo
¡Copiado!
110€
Aplicable en compras de importe superior a 650€ hasta el 10/6/2026.
¡Copiado!
65€
Aplicable en compras de importe superior a 459€ hasta el 10/6/2026.
¡Copiado!
65€
Aplicable en compras de importe superior a 459€ hasta el 10/6/2026.
¡Copiado!
45€
Aplicable en compras de importe superior a 319€ hasta el 10/6/2026.
¡Copiado!
45€
Aplicable en compras de importe superior a 319€ hasta el 10/6/2026.
¡Copiado!
30€
Aplicable en compras de importe superior a 209€ hasta el 10/6/2026.
¡Copiado!
30€
Aplicable en compras de importe superior a 209€ hasta el 10/6/2026.
¡Copiado!
20€
Aplicable en compras de importe superior a 139€ hasta el 10/6/2026.
¡Copiado!
20€
Aplicable en compras de importe superior a 139€ hasta el 10/6/2026.
¡Copiado!
10€
Aplicable en compras de importe superior a 69€ hasta el 10/6/2026.
¡Copiado!
10€
Aplicable en compras de importe superior a 69€ hasta el 10/6/2026.
¡Copiado!
6€
Aplicable en compras de importe superior a 39€ hasta el 10/6/2026.
¡Copiado!
6€
Aplicable en compras de importe superior a 39€ hasta el 10/6/2026.
¡Copiado!
3€
Aplicable en compras de importe superior a 15€ hasta el 10/6/2026.
¡Copiado!
3€
Aplicable en compras de importe superior a 15€ hasta el 10/6/2026.