Be Smart, Go Local.

DIY Info Display with LILYGO T-Display-S3 in ESPHome

Guide for creating a Home Assistant-compatible status screen and controller with LILYGO T-Display S3 AMOLED board and ESPHome.

Digging through AliExpress, I came across this interesting LILYGO T-Display S3 AMOLED screen board. The T-Display S3 is a development board featuring an ESP32-S3 microcontroller and a built-in AMOLED display, ideal for IoT projects with vibrant visual output and more then enough processing power for small projects.

DIY Status Screen Controller LILYGO T-Display S3 Amoled Featured Image

In this article, I’m sharing my process of flashing, setting up and rendering content on this AMOLED development board using ESPHome’s display engine. You can use it to push data from Home Assistant, create neat status displays and even use it as a controller.

The LILYGO T-Display-S3 AMOLED version is available on AliExpress as well as Amazon.

Different Versions of the LILYGO T-Display S3

Before diving further, it’s important to note that both versions of this board—Basic and Touch—are almost the same, with only a few differences in pinouts and features. These distinctions primarily relate to the touch functionality in the Touch version with it having an additional Touch_INT pin on GPIO21 and a Power Enable pin on GPIO38 to support the touchscreen, which are not present in the Basic version.

Currently, version 2.0 of this board has the following pinout:

  • LILYGO T-Display S3 AMOLED Basic
  • ESP32-S3R8 Dual-core LX7
  • AMOLED 1.91 inches 240×536
  • QSPI RM67162
  • 16MB Flash, 8MB PSRAM
  • Wi-Fi 802.11, BLE 5.0
  • LILYGO T-Display S3 AMOLED Touch
  • ESP32-S3R8 Dual-core LX7
  • AMOLED Touch 1.91 inches 240×536
  • QSPI RM67162
  • 16MB Flash, 8MB PSRAM
  • Wi-Fi 802.11, BLE 5.0

This guide is based on the AMOLED Basic version.

Initial Flash in ESPHome

Before we can setup the display component and actually render things on this board, it needs to be initialized in ESPHome. Create a new device in ESPHome, name it whatever you like and initialize it with the following code:

esphome:
  name: lilygos3
  friendly_name: LILYGO-S3 T-Display  
  platformio_options:
    build_unflags: -Werror=all
    board_build.flash_mode: dio

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: esp-idf   

api:
  encryption:
    key: "XXXXX" #Replace

ota:
  - platform: esphome
    password: "XXXXX" #Replace

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

  ap:
    ssid: "S3 Fallback Hotspot"
    password: "zQ9tuPKIfFMu"

logger:

After the initial flash, nothing will be rendered on the screen. Since we haven’t configured the pinout for the spi and display components, the board will simply bootup and you will be able to access it wirelessly from your network.

The next step is to configure the display component, add a text font and render something simple using ESPHome’s display rendering engine. For this to work, the proper pins need to be set. From the official pinout images, here’s how the configuration should look like for the LILYGO T-Display S3 Basic Version:

spi:
  id: qspi_bus
  type: quad
  clk_pin: GPIO47
  data_pins:
    - GPIO18
    - GPIO7
    - GPIO48
    - GPIO5

display:
  - platform: qspi_amoled
    model: RM67162
    id: s3_display
    update_interval: 1s
    dimensions:
      height: 536
      width: 240
    color_order: rgb
    brightness: 255
    cs_pin: GPIO6
    reset_pin: GPIO17
    rotation: 0
    lambda: |-
      it.print(10, 20, id(font_roboto), Color(255, 255, 255), "Hello, SmartHomeScene!");

#Add a simple Google Font
font:
  - file: "gfonts://Roboto"
    id: font_roboto
    size: 20

If you get something on the display, it means your configuration is correct and working:

DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Hello SmartHomeScene
Initial flash: “Hello, SmartHomeScene!”

Understanding ESPHome’s Display Rendering Engine

In order to create something useful on the LILYGO T-Display S3, ESPHome’s display rendering engine needs to be used. This engine can handle tasks like drawing shapes, displaying text with custom fonts, and even rendering images. This flexibility is achieved through ESPHome’s lambda system, which allows you to write C++ code to control the display.

In the lambda, you can write code like in any lambda in ESPHome. Display lambdas are additionally passed a variable called it which represents the rendering engine object. When rendering display coordinates in ESPHome, the origin (0, 0) starts from the top-left corner of the screen. X-coordinates increase to the right, while Y-coordinates increase downward. Colors are rendered using RGB values, typically in the format 1, where values range from 0-255 for each channel. Fonts are defined by specifying typefaces and sizes, allowing you to print text in various styles. Images are pushed to the display using bitmap data, typically preloaded or fetched via code, and rendered based on coordinate placement.

Here’s how a red circle centered in the middle looks like, using two helpers to get width and height of the screen automatically:

lambda: |-
  // Draw a red circle in the middle of the display
  it.filled_circle(it.get_width() / 2, it.get_height() / 2, 120, Color(255, 0, 0));
DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Red Circle Test
Flashing a red circle

Rotating the screen

Before the screen can be fully useful, it’s important to understand the rotation configuration parameter in ESPHome. You can add a rotation to the screen and set it to 90, 180 or 270 degrees in order to flip the components on the screen.

It’s very important to note that rotation is tightly correlated with the get_width() and get_height() helpers in ESPHome. When set to 90 or 270 degrees, the width and height are swapped, meaning what was previously considered the width (longer axis) becomes the height and vice versa. This ensures the dimensions adapt correctly to the display’s orientation, allowing proper rendering regardless of the rotation angle.

For example, if I draw a simple triangle spanning the available width and height, starting with 0, 0 coordinates, here’s how the helper would influence it’s size:

DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Rotation 0 Degrees
Rotation 0°, dynamic width and height
DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Rotation 180 Degrees
Rotation 180°, dynamic width and height
DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Rotation 90 Degrees
Rotation 90°, dynamic width and height
DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Rotation 270 Degrees
Rotation 270°, dynamic width and height

Since the screen’s width and height (X and Y axes) are fetched dynamically, the triangle’s size will vary based on the display dimensions. Keep this in mind when rendering content, as it can affect the layout. This will help you avoid frustrations and the need to constantly re-flash the device during testing.

Adding Fonts

Fonts can be added to ESPHome’s display component by uploading them locally or simply using Google Fonts. You need to assign IDs to each font, define it’s size and any extra characters if necessary. The ID is called in the lambda function of the display component, which will render the text you are asking it to, with the corresponding font. Here’s how fonts can be defined:

font:
  - file: "fonts/Comic Sans MS.ttf"
    id: comic_sans_20
    size: 20
    bpp: 2
  - file: "gfonts://Roboto"
    id: roboto_20
    size: 20
  - file:
      type: gfonts
      family: Roboto
      weight: 900
    id: roboto_16
    size: 16
....

You can check out more examples and configuration parameters in ESPHome’s Font Component.

Adding Images and Icons

Very similarly to fonts, images can be loaded onto ESP32 devices with compatible screens. The Image Component also requires a unique ID, which is called upon in the lambda function of the display component. Here’s how images or icon can be defined and resized:

image:
  - file: "smarthomescene.png" #Local image
    id: my_image
    resize: 100x100
    type: RGBA #full color image
image:
  - file: mdi:alert-outline #MDI Icon
    id: alert
    resize: 80x80
image:
  - file: https://esphome.io/_images/logo.png #URL image
    id: esphome_logo
    resize: 200x162

For example, let’s say I want to render a local image at the center of the LILYGO T-Display S3. I need to upload the image in the ESPHome folder where the main .yaml file is located. Or, I could use another folder within this directory.

DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: SmartHomeScene Logo
Flashing an image, SmartHomeScene’s Logo
display:
.......
  lambda: |-
      // Render the image in the center of the display
      it.image(it.get_width() / 2 - 175, it.get_height() / 2 - 77, id(smarthomescene)); 


image:
  - file: "the-logo.png"
    id: smarthomescene
    resize: 350x155   
    type: RGBA

To center the image on the S3 display, you need to calculate the offset from the display’s center by subtracting half the image’s dimensions. First, divide the display width by 2 to get the horizontal center. Then, subtract half the image’s width to position it correctly. For example, with a 350-pixel wide image (the-logo.png), you subtract 175 (half the width). The same applies vertically: divide the display height by 2 and subtract half the image’s height (77 for a 155-pixel tall image). This ensures the image is centered on both axes.

Exposing Entities from Home Assistant

Just like any other ESPHome device, you can expose sensors and entities from Home Assistant to the LILYGO S3 and use it as a status display or controller. This can include any entity you wish, preferably some that you find a useful functionality for. For example, let’s say I want to use this display to showcase my office temperature and humidity with some nice icons. I want them laid out horizontally in the middle of the screen on the right side with an interesting and readable font. I want to keep the logo smaller on the left side:

DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: Temperature and Humidity
Flashing temperature and humidity sensors from Home Assistant

First, I need to make the two sensors from Home Assistant available to the LILYGO board by adding this to the configuration:

sensor:
  - platform: homeassistant
    entity_id: sensor.office_sensor_temperature
    id: office_temperature
    name: "Office Temperature"
  - platform: homeassistant
    entity_id: sensor.office_sensor_humidity
    id: office_humidity
    name: "Office Humidity"

Next, I need to add the font and images (and icons) with an appropriate size. 60×60 pixels works nicely for the icons in my example, while keeping the image at 200 width corresponds to about half the display with some space in between:

font:
  - file: "gfonts://Audiowide"
    id: audiowide_font
    size: 40
image:
  - file: mdi:thermometer
    id: temperature_icon
    resize: 60x60  
  - file: mdi:water_percent
    id: humidity_icon
    resize: 60x60    
  - file: "the-logo.png"
    id: smarthomescene
    resize: 200x88     
    type: rgba  

And finally, add the display rendering lambda to the code. This function uses the get_height helper to read screen height and use it to center the image vertically (smarthomescene). The rest of the function statically sets the X and Y coordinates for the sensors with the appropriate fonts and positions them on the screen:

    lambda: |-
      int screen_height = it.get_height();
      it.image(20, (screen_height - 88) / 2, id(smarthomescene));
      it.image(260, 40, id(temperature_icon));
      it.printf(320, 40, id(audiowide_font), Color(255, 255, 255), "%.1f°C", id(office_temperature).state);
      it.image(260, 140, id(humidity_icon));
      it.printf(320, 140, id(audiowide_font), Color(255, 255, 255), "%.1f%%", id(office_humidity).state);

Cycling Multiple Display Pages

ESPHome’s display component allows you to render multiple pages with different contents and cycle through them on a timer or on any input events. As the LILYGO T-Display S3 AMOLED display is equipped with two buttons on the front, we can use one of the buttons to change the information shown on the screen. The first button uses GPIO0, which is a strapping pin so we want to avoid that. The second one uses GPIO21, which is free to be used in this automation.

For this example, I created a second page with a greeting message, kept the original one with temperature and humidity as a second and created a third page with some basic info of my system monitor:

DIY Status Screen Controller LILYGO T-Display S3 Amoled Initial Flashing: 3 Pages Cycling
Cycling through 3 pages
display:
  - platform: qspi_amoled
    ...........
    pages:
      - id: page1
        lambda: |-
          it.printf(10, 100, id(audiowide_font), Color(255, 255, 255), "Hello SmartHomeScene!");
      
      - id: page2
        lambda: |-
          int screen_height = it.get_height();
          it.image(20, (screen_height - 88) / 2, id(logo));
          it.image(260, 40, id(temperature_icon));
          it.printf(320, 40, id(audiowide_font), Color(255, 255, 255), "%.1f°C", id(office_temperature).state);
          it.image(260, 140, id(humidity_icon));
          it.printf(320, 140, id(audiowide_font), Color(255, 255, 255), "%.1f%%", id(office_humidity).state);

      - id: page3
        lambda: |-
            // CPU Usage
            it.image(20, 20, id(cpu_icon));  // CPU icon
            it.printf(100, 20, id(audiowide_font), Color(255, 255, 255), "CPU: %.1f%%", id(system_monitor_processor_use).state);

            // Memory Usage
            it.image(20, 100, id(memory_icon));  // Memory icon
            it.printf(100, 100, id(audiowide_font), Color(255, 255, 255), "RAM: %.1fMB", id(system_monitor_memory_use).state);

            // Disk Usage
            it.image(20, 180, id(harddisk_icon));  // Disk icon
            it.printf(100, 180, id(audiowide_font), Color(255, 255, 255), "DISK: %.1fGB", id(system_monitor_disk_use).state);


#Button on GPIO21 as trigger for page cycling
binary_sensor:
  - platform: gpio
    pin: GPIO21
    name: "Page Switch Button"
    on_press:
      then:
        - display.page.show_next: s3_display
        - component.update: s3_display

# Fonts and icons for each image used
font:
  - file: "gfonts://Audiowide"
    id: audiowide_font
    size: 40
image:
  - file: mdi:thermometer
    id: temperature_icon
    resize: 60x60  
  - file: mdi:cpu-64-bit
    id: cpu_icon
    resize: 60x60
............  

# Exposed sensors from Home Assistant
sensor:
  - platform: homeassistant
    entity_id: sensor.office_sensor_temperature
    id: office_temperature
    name: "Office Temperature"
    name: "CPU Usage"
  - platform: homeassistant
    entity_id: sensor.system_monitor_memory_use
    id: system_monitor_memory_use
    name: "Memory Usage"
.............

Summary

There’s so much that can be done with ESPHome’s display rendering engine. It’s a powerful and flexible tool, but hardly simple to use. In my experience, it requires a lot of flashing, tweaking and re-flashing to get the desired effect you are after. If this article is not enough to get you started, I highly suggest you go over the official documentation and try to understand at least the most basic parameters before flashing this LILYGO T-Display S3 AMOLED screen.

The LILYGO T-Display S3 AMOLED development board is available on AliExpress and Amazon. Here’s a few links of where you can purchase the device:

DIY Status Screen Controller LILYGO T-Display S3 Amoled Buying Links


AliExpress | AliExpress | AliExpress


United States | United Kingdom
Germany | Netherlands | France
Italy | Spain | Sweden
*If links fail to open, try disabling your AdBlocker.

After tweaking my code so many times, I managed to display some interesting data about my office setup. Here’s the full config from this article:

11 thoughts on “DIY Info Display with LILYGO T-Display-S3 in ESPHome”

  1. Awesome article, as usual. Thank you!

    I’m curious about the touch screen capability. Can you program button or switch elements to display and use touch to activate and deactivate them?

    Reply
  2. Any reviews/guide coming up from that Yellow ESP 2.8 inch display.
    its around 10 euro’s and should be suitable for a all in one solution, if you want a simple display for sensor information.

    Reply
  3. Wow! Excellent timing and excelent intro! I was looking at this few weeks ago as a solution to display outside temp in the lobby of our house because there aren’t any not pricy smart OoB solutions for that and I came through this but I found that too much complicated for the setup. Thanks to this article I know the basics and I’m confident in making it and I think I van even extent my original plan to show weather forcast, travel time to work from Waze and upcomming bus departures.

    Reply
    • Yes, there is no limit actually to what you can achieve with this. The touch version is even more flexible.
      I want to re-do the whole thing in LGVL, which should be easier and much more capable.

      Reply
  4. I’ve been looking for a functional code for about 2 years and now I could finally make this screen to work. Thank you so much, I was about to throw away these boards.

    Reply
  5. Humm.. as soon as I added the DISPLAY configuration the captive web portal stop working and the OTA update also is struggling to make changes. Something on this DISPLAY config interferes with the device capabilities to run its internal webserver.

    Reply

Leave a Comment