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 proFeatures
- 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:
.jpg)
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();'