Skip to content

Powered by Grav

T-Connect Pro

LilyGO T-Connect Pro ESPHome Example with LoRa and Display

A complete guide to configure LilyGO T-Connect Pro as a smart LoRa device using ESPHome with Home Assistant integration.

Full specification of device: LilyGO T-connect pro

Features

  • LoRa Communication: Send and receive LoRa messages directly from Home Assistant
  • Energy Monitoring Display: Real-time LVGL display showing voltage and power data from Home Assistant sensors via WiFi
  • Touchscreen Interface: Full touchscreen support for navigation and control
  • OTA Updates: Over-the-air firmware updates capability
  • HA Status Indicator: Visual indicator showing Home Assistant connection status
  • Multi-page UI: Switch between energy monitor, LoRa status, and software info pages

Compatibility

This example works seamlessly with the Heltec V4 configuration, which also supports bidirectional LoRa communication.

Display - energy monitor:

lora_example%20%285%29

Full code:

YAML
esphome:
  name: lilygo-t-connect-pro
  friendly_name: lilygo T-connect-pro

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
  cpu_frequency: 240MHz
psram:
  mode: octal # Optional, depends on your hardware (quad, octal, or hex)
  speed: 80MHz 
# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "7zlzZPqliip3u0eKhZFzCnlxapcXOZQWSxxxxxxxxxx=" # It needs to replace yours
  on_client_connected:
    - if:
        condition:
          lambda: 'return (0 == client_info.find("Home Assistant "));'
        then:
          - lvgl.widget.show: lbl_hastatus
  on_client_disconnected:
    - if:
        condition:
          lambda: 'return (0 == client_info.find("Home Assistant "));'
        then:
          - lvgl.widget.hide: lbl_hastatus        

ota:
  - platform: esphome
    password: "14cf8f772d980cae800032xxxxxxxxxx" # It needs to replace yours

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Lilygo-T-Connect-Pro"
    password: "wlJ7KcrooTXm"

#captive_portal:

i2c:
  - id: bus_a
    sda: GPIO39
    scl: GPIO40

spi:
    clk_pin: GPIO12
    mosi_pin: GPIO11
    miso_pin: GPIO13
    interface: SPI3
output:
  - platform: ledc
    pin: GPIO48
    id: backlight_pwm
light:
  - platform: monochromatic
    output: backlight_pwm
    name: "Display Backlight"
    id: backlight
    default_transition_length: 0s    
globals:
  - id: muon_cnt
    type: int
    initial_value: "1"
  - id: background_cnt
    type: int
    initial_value: "1"
  - id: loop_counter
    type: int
    initial_value: '0'
  - id: loop_running
    type: bool
    initial_value: 'true'
  - id: arrow_anim_start_x
    type: int
    initial_value: "64"
  - id: arrow_anim_end_x
    type: int
    initial_value: "74"
  - id: last_sent_text
    type: std::string
    initial_value: '"(none)"'
  - id: received_text
    type: std::string
    initial_value: '"(none)"'    
font:
  - file: "gfonts://Nova+Mono"
    id: nova_mono_16
    size: 16
  - file: "gfonts://Roboto"
    id: roboto_10
    size: 10
  - file:
      type: gfonts
      family: Roboto
      weight: 900
    id: roboto_24
    size: 24
text:
  - platform: template
    name: "Message"
    id: lora_message
    optimistic: true
    min_length: 0
    max_length: 50
    mode: text

button:
  - platform: template
    name: "Transmit Packet"
    on_press:
      then:
        - sx126x.send_packet:
            data: !lambda |-
              std::string text = id(lora_message).state;
              if (text.empty()) text = "EMPTY";
              if (text.length() > 50) text = text.substr(0, 50);
              id(last_sent_text) = text;
              ESP_LOGD("LoRa", "Sending: %s", text.c_str());
              return std::vector<uint8_t>(text.begin(), text.end()); 
        - lvgl.label.update:
            id: last_send_lora_message
            text: !lambda 'return id(last_sent_text).c_str();'
        - logger.log: "Sent using LoRa"
  - platform: template
    name: "Transmit Packet2"
    on_press:
      then:
        - sx126x.send_packet:
            data: !lambda |-
             std::string text = "Hello World!";
             id(last_sent_text) = text;
             return std::vector<uint8_t>(text.begin(), text.end());
        - lvgl.label.update:
            id: last_send_lora_message
            text: !lambda 'return id(last_sent_text).c_str();'             
        - logger.log: Button Pressed
sensor:
  - platform: homeassistant
    id: ha_L1_voltage  # Your unique sensor ID in ESPHome
    name: "L1 Voltage"  # Name (optional, visible in ESPHome)
    entity_id: sensor.pomiarpradu_channel_a_voltage  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional  
    on_value:
      then:
        - lvgl.label.update:
            id: l1_voltage
            text:
              format: "L1=%.1fV"
              args: [id(ha_L1_voltage).state]
              if_nan: "N/A"
  - platform: homeassistant
    id: ha_L2_voltage  # Your unique sensor ID in ESPHome
    name: "L2 Voltage"  # Name (optional, visible in ESPHome)
    entity_id: sensor.pomiarpradu_channel_b_voltage  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional  
    on_value:
      then:
        - lvgl.label.update:
            id: l2_voltage
            text:
              format: "L2=%.1fV"
              args: [id(ha_L2_voltage).state]
              if_nan: "N/A"
  - platform: homeassistant
    id: ha_L3_voltage  # Your unique sensor ID in ESPHomee
    name: "L3 Voltage"  # Name (optional, visible in ESPHome)
    entity_id: sensor.pomiarpradu_channel_c_voltage  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional  
    on_value:
      then:
        - lvgl.label.update:
            id: l3_voltage
            text: 
              format: "L3=%.1fV"
              args: [id(ha_L3_voltage).state]
              if_nan: "N/A"   
  - platform: homeassistant
    id: ha_L1_power  # Your unique sensor ID in ESPHome
    name: "L1 Power"  # Name (optional, visible in ESPHome)
    entity_id: sensor.pomiarpradu_channel_a_power  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional       
    on_value:
      then:
        - lvgl.label.update:
            id: l1_power
            text: 
              format: "L1=%.1fW"
              args: [id(ha_L1_power).state]
              if_nan: "N/A"               
  - platform: homeassistant
    id: ha_L2_power  # Your unique sensor ID in ESPHome
    name: "L2 Power"  # Name (optional, visible in ESPHome)
    entity_id: sensor.pomiarpradu_channel_b_power  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional  
    on_value:
      then:
        - lvgl.label.update:
            id: l2_power
            text: #"L2=-2589.9W"
              format: "L2=%.1fW"
              args: [id(ha_L2_power).state]
              if_nan: "N/A"               
  - platform: homeassistant
    id: ha_L3_power  # Your unique sensor ID in ESPHome
    name: "L3 Power"  # Name (optional, visible in ESPHome)
    entity_id: sensor.pomiarpradu_channel_c_power  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional  
    on_value:
      then:
        - lvgl.label.update:
            id: l3_power
            text: 
              format: "L3=%.1fW"
              args: [id(ha_L3_power).state]
              if_nan: "N/A"               
  - platform: homeassistant
    id: ha_totalpower_l1_l2_l3  # Your unique sensor ID in ESPHome
    name: "L1_L2_L3 Total Power"  # Name (optional, visible in ESPHome)
    entity_id: sensor.totalpower_l1_l2_l3  # Change to your HA ID entity, ex. sensor.your_voltage
    internal: true  # default true, no back export back to HA
    accuracy_decimals: 1  # optional        
    on_value:
      then:
        - lvgl.label.update:
            id: l1_l2_l3_power
            text: 
              format: "Total=%.1fW"
              args: [id(ha_totalpower_l1_l2_l3).state]
              if_nan: "N/A"               
image:
  - file: mdi:transmission-tower
    id: grid
    type: RGB565
    transparency: alpha_channel
    resize: 100x100
  - file: mdi:home-city-outline
    id: home
    type: RGB565
    transparency: alpha_channel
    resize: 100x100
  - file: mdi:arrow-left-bold
    id: arrow_left
    resize: 32x32
    type: RGB565
    transparency: alpha_channel

  - file: mdi:arrow-right-bold
    id: arrow_right
    resize: 32x32
    type: RGB565
    transparency: alpha_channel    
touchscreen:
  - platform: cst226
    id: my_touchscreen
    interrupt_pin: GPIO3
    reset_pin: GPIO47
    transform:
      mirror_x: true
      mirror_y: false
      swap_xy: true
    on_touch:
    - lambda: |-
          ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
              touch.x,
              touch.y,
              touch.x_raw,
              touch.y_raw
              );
color:
  - id: color_red
    hex: 'FF0000'
  - id: color_green
    hex: '008000' # Custom green

display:
  - platform: ili9xxx
    model: ST7796
    id: my_display
    dimensions:
      height: 480
      width: 222
      offset_height: 0
      offset_width: 49
    rotation: 270
#  transform:
#  swap_xy: true
#  mirror_x: false
#  mirror_y: true
    color_order: bgr
    invert_colors: true
    cs_pin: GPIO21
    dc_pin: GPIO41
    reset_pin: GPIO46
#    show_test_card: true

lvgl:
  displays:
    - my_display
  #buffer_size: 30%
  style_definitions:
    - id: my_header
      bg_color: 0x4A90E2
      text_color: white
      pad_all: 3
    - id: my_box
      bg_color: 0x2B2B2B
      border_width: 1
      border_color: 0xAAAAAA
      radius: 4
      pad_all: 3
      text_color: white
    - id: header_footer
      bg_color: 0x2F8CD8
      bg_grad_color: 0x005782
      bg_grad_dir: VER
      bg_opa: COVER
      border_opa: TRANSP
      radius: 0
      pad_all: 0
      pad_row: 0
      pad_column: 0
      border_color: 0x0077b3
      text_color: 0xFFFFFF
      width: 100%
      height: 40      
  pages:
    - id: main_page
      widgets:
        - obj: # main background
            x: 0
            y: 0
            height: 100%
            width: 100%
            bg_color: 0xD2D2D2
            border_width: 0
            pad_all: 0
            widgets:
          # --- Header ---
              - obj:
                  x: 0
                  y: 0
                  height: 161
                  width: 126
                  pad_all: 0
                  bg_color: 0xFBEC5D
                  border_width: 1
                  border_color: 0x000000
                  widgets:
                    - label:
                        x: 5
                        y: 3
                        text: "Power:"
                        text_color: 0x001400
                        text_font: roboto_24       
              - obj:
                  x: 0
                  y: 40
                  height: 40
                  width: 126
                  pad_all: 0
                  bg_color: 0xA47449
                  border_width: 0
                  widgets:
                    - label:
                        id: l1_voltage
                        x: 3
                        y: 5
                        text: "L1=NaN"
                        text_color: 0xFFFFFF
                        text_font: roboto_24
              - obj:
                  x: 0
                  y: 80
                  height: 40
                  width: 126
                  pad_all: 0
                  bg_color: 0x000000
                  border_width: 0
                  widgets:
                    - label:
                        id: l2_voltage
                        x: 3
                        y: 5
                        text: "L2=NaN"
                        text_color: 0xFFFFFF
                        text_font: roboto_24        
              - obj:
                  x: 0
                  y: 120
                  height: 40
                  width: 126
                  pad_all: 0
                  bg_color: 0x727272
                  border_width: 0
                  widgets:
                    - label:
                        id: l3_voltage
                        x: 3
                        y: 5
                        text: "L3=NaN"
                        text_color: 0xFFFFFF
                        text_font: roboto_24                                                                              
              - obj:
                  x: 126
                  y: 0
                  height: 40
                  width: 354
                  pad_all: 0
                  bg_color: 0x6395EE
                  border_width: 0
                  widgets:
                    - label:
                        x: 25
                        y: 3
                        text: "ENERGY DISTRIBUTION"
                        text_color: 0xFFFFFF
                        text_font: roboto_24
              - obj:
                  x: 126
                  y: 40
                  height: 142
                  width: 354
                  pad_all: 0
                  bg_color: 0xF2F2F2
                  border_width: 0
                  widgets:
                    - image:
                        x: -12
                        src: grid
                        y: 5          
                    - image:
                        x: 252
                        src: home
                        y: 5    
                    - label:
                        id: l1_power
                        x: 101
                        y: 5
                        text: "L1:NaN"
                        text_color: 0x000000
                        text_font: roboto_24     
                    - image:
                        id: l1_arrow
                        x: !lambda 'return id(arrow_anim_start_x);'
                        y: 3
                        src: arrow_right
                        hidden: false             
                    - label:
                        id: l2_power
                        x: 101
                        y: 35
                        text: "L2:NaN"
                        text_color: 0x000000
                        text_font: roboto_24    
                    - image:
                        id: l2_arrow
                        x: !lambda 'return id(arrow_anim_start_x);'
                        y: 33
                        src: arrow_right
                        hidden: false                                                        
                    - label:
                        id: l3_power
                        x: 101
                        y: 65
                        text: "L3:NaN"
                        text_color: 0x000000
                        text_font: roboto_24                            
                    - image:
                        id: l3_arrow
                        x: !lambda 'return id(arrow_anim_start_x);'
                        y: 63
                        src: arrow_right
                        hidden: false                           
                    - label:
                        id: l1_l2_l3_power
                        x: 100
                        y: 105
                        text: "Total:NaN"
                        text_color: 0x000000
                        text_font: roboto_24                            
                    - image:
                        id: l1_l2_l3_arrow
                        x: 3
                        y: 105
                        src: arrow_right
                        hidden: false       
    - id: lora_page
      widgets:
        # ===== HEADER =====
        - obj:
            x: 0
            y: 0
            height: 40
            width: 480
            pad_all: 0
            bg_color: 0x6395EE
            border_width: 0
            widgets:
              - label:
                  x: 25
                  y: 3
                  text: "LORA status"
                  text_color: 0xFFFFFF
                  text_font: roboto_24
        # ===== CONTENT =====
        - obj:
            y: 40
            width: 100%
            height: 160
            widgets:
              - label:
                  y: 0
                  id: last_send_lora_message
                  text: "Last send message"
                  text_font: roboto_24
              - label:
                  y: 40
                  id: last_recived_lora_message
                  text: "Last recived message"
                  text_font: roboto_24
    - id: ekosterowanie_page
      widgets:
        # ===== HEADER =====
        - obj:
            x: 0
            y: 0
            height: 40
            width: 480
            pad_all: 0
            bg_color: 0x6395EE
            border_width: 0
            widgets:
              - label:
                  x: 25
                  y: 3
                  text: "Software"
                  text_color: 0xFFFFFF
                  text_font: roboto_24
        # ===== CONTENT =====
        - obj:
            y: 40
            width: 100%
            height: 160
            widgets:
              - label:
                  y: 0
                  text: "ekosterowanie.pl"
                  text_font: roboto_24
              - label:
                  y: 40
                  text: "software.ekosterowanie.pl"
                  text_font: roboto_24        
              - label:
                  y: 80
                  text: "Do you need dedicated software? Contact us!"
                  text_font: nova_mono_16                                                                                     
  top_layer:
    widgets:
      - buttonmatrix:
          align: bottom_mid
          styles: header_footer
          pad_all: 0
          outline_width: 0
          id: top_layer
          items:
            styles: header_footer
          text_font: montserrat_24
          rows:
            - buttons:
              - id: page_prev
                text: "\uF053"
                on_press:
                  then:
                    lvgl.page.previous:
              - id: page_home
                text: "\uF015"
                on_press:
                  then:
                    lvgl.page.show: main_page
              - id: page_next
                text: "\uF054"
                on_press:
                  then:
                    lvgl.page.next:          
      - label:
          text: "\uF1EB"
          id: lbl_hastatus
          hidden: true
          align: top_right
          x: -4
          y: 7
          text_align: right
          text_color: 0xFFFFFF                                         
interval:
  - interval: 2s
    then:
      - script.execute:
          id: update_arrows
      - if:
          condition:
            and:
              - lambda: 'return id(loop_running);'
              - lambda: 'return id(loop_counter) < 10000;'
          then:
            # for testing purposes we send LoRa message based on counter
            - sx126x.send_packet:
                data: !lambda |-
                  char buf[16];
                  int next_val = id(loop_counter) + 1;
                  snprintf(buf, sizeof(buf), "%d", next_val);
                  std::string msg(buf);
                  id(last_sent_text) = msg;
                  return std::vector<uint8_t>(buf, buf + strlen(buf));
            - globals.set:
                id: loop_counter
                value: !lambda 'return id(loop_counter) + 1;'
            - lvgl.label.update:
                id: last_send_lora_message
                text: !lambda 'return id(last_sent_text).c_str();'                
            - logger.log:
                format: "Send: %d"
                args: [ 'id(loop_counter)' ]
          else:
            - if:
                condition: 
                  lambda: 'return id(loop_running);'
                then:
                  - logger.log: "loop ended with 10000"
                  - globals.set:
                      id: loop_running
                      value: 'false'
script:
  - id: update_arrows
    then:
      - if:
          condition:
            lambda: 'return !isnan(id(ha_L1_power).state) && id(ha_L1_power).state != 0;'
          then:
            - if:
                condition:
                  lambda: 'return id(ha_L1_power).state > 0;'
                then:
                  # ➡️ to home - red
                  - lvgl.image.update:
                      id: l1_arrow
                      src: arrow_right
                      image_recolor: color_red
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l1_power
                      text_color: color_red
                else:
                  # ⬅️ to grid – green
                  - lvgl.image.update:
                      id: l1_arrow
                      src: arrow_left
                      image_recolor: color_green
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l1_power
                      text_color: color_green
            # 🔁 simple animation
            - lvgl.widget.update:
                id: l1_arrow
                x: !lambda |-
                  static bool toggle = false;
                  toggle = !toggle;
                  return toggle ? id(arrow_anim_start_x) : id(arrow_anim_end_x);
      - if:
          condition:
            lambda: 'return !isnan(id(ha_L2_power).state) && id(ha_L2_power).state != 0;'
          then:
            - if:
                condition:
                  lambda: 'return id(ha_L2_power).state > 0;'
                then:
                  # ➡️ to home – red
                  - lvgl.image.update:
                      id: l2_arrow
                      src: arrow_right
                      image_recolor: color_red
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l2_power
                      text_color: color_red
                else:
                  # ⬅️ to grid – green
                  - lvgl.image.update:
                      id: l2_arrow
                      src: arrow_left
                      image_recolor: color_green
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l2_power
                      text_color:  color_green
            # 🔁 simple animation
            - lvgl.widget.update:
                id: l2_arrow
                x: !lambda |-
                  static bool toggle = false;
                  toggle = !toggle;
                  return toggle ? id(arrow_anim_start_x) : id(arrow_anim_end_x);     
      - if:
          condition:
            lambda: 'return !isnan(id(ha_L3_power).state) && id(ha_L3_power).state != 0;'
          then:
            - if:
                condition:
                  lambda: 'return id(ha_L3_power).state > 0;'
                then:
                  # ➡️ to home – red
                  - lvgl.image.update:
                      id: l3_arrow
                      src: arrow_right
                      image_recolor: color_red
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l3_power
                      text_color: color_red
                else:
                  # ⬅️ to grid – green
                  - lvgl.image.update:
                      id: l3_arrow
                      src: arrow_left
                      image_recolor: color_green
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l3_power
                      text_color:  color_green
            # 🔁 simple animation
            - lvgl.widget.update:
                id: l3_arrow
                x: !lambda |-
                  static bool toggle = false;
                  toggle = !toggle;
                  return toggle ? id(arrow_anim_start_x) : id(arrow_anim_end_x);       
      - if:
          condition:
            lambda: 'return !isnan(id(ha_totalpower_l1_l2_l3).state) && id(ha_totalpower_l1_l2_l3).state != 0;'
          then:
            - if:
                condition:
                  lambda: 'return id(ha_totalpower_l1_l2_l3).state < 0;'
                then:
                  # ➡️ to home – red
                  - lvgl.image.update:
                      id: l1_l2_l3_arrow
                      src: arrow_right
                      image_recolor: color_red
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l1_l2_l3_power
                      text_color: color_red
                else:
                  # ⬅️ to grid – green
                  - lvgl.image.update:
                      id: l1_l2_l3_arrow
                      src: arrow_left
                      image_recolor: color_green
                      image_recolor_opa: 100%
                  - lvgl.label.update:
                      id: l1_l2_l3_power
                      text_color:  color_green
            # 🔁 simple animation
            - lvgl.widget.update:
                id: l1_l2_l3_arrow
                x: !lambda |-
                  static bool toggle = false;
                  toggle = !toggle;
                  return toggle ? id(arrow_anim_start_x) : id(arrow_anim_end_x);                                                    
sx126x:
  dio1_pin: GPIO45
  cs_pin: GPIO14
  busy_pin: GPIO38
  rst_pin: GPIO42
  pa_power: 22
  bandwidth: 125_0kHz
  crc_enable: true
  frequency: 868000000
  modulation: LORA
  hw_version: sx1262
  rf_switch: true
  sync_value: [0x14, 0x24]
  preamble_size: 10 #8
  spreading_factor: 10 #7
  coding_rate: CR_4_7 #CR4_6
  tcxo_voltage: 1_8V
  tcxo_delay: 5ms
  on_packet:
    then:
      - lambda: |-
          ESP_LOGD("lambda", "packet %s", format_hex(x).c_str());
          ESP_LOGD("lambda", "rssi %.2f", rssi);
          ESP_LOGD("lambda", "snr %.2f", snr);
          id(received_text) = std::string(x.begin(), x.end());
      - lvgl.label.update:
          id: last_recived_lora_message
          text: !lambda 'return id(received_text).c_str();'

© 2026 Ekosterowanie. All rights reserved.