Hey you poor bastard trying to add a cheap 4 inch ST7796 touchscreen from Ali/Temu whatever to HA. Hopefully you found this thread sooner rather than later, but in any case, here we go.
So, the screen is weird. To make sure its the same one, here are pics:
It uses an ST7796 driver, but whatever you found so far, at least at the time of this writing, ilixxx drivers do not work.
This Github thread was a good starting point, but I could not make the ilixxx drivers work.
What I ended up doing instead is using the mipi_spi display driver. It’s … not great, but it’s the only one I could wrestle into submission on this panel.
That being, said, the driver is also lacking (or at least I could not find them) proper rotation and mirroring functions, so I had to get a bit … creative in solving some problems. Basically the screen is physically rotated 180 and since the touch coordinates are now wonky, I corrected that in code. If it’s stupid but it works … ![]()
Anywho, the config gives you:
1: The wiring I used for screen and touch, just read the pins from the Yaml
2: A fully functional, but wonky, screen
3: Fully functioning touch functionality with entities that show in HA the coords where a touch occurred.
globals:
- id: last_color
type: std::string
initial_value: "\"\""
font:
- file: "gfonts://Roboto"
id: default_font
size: 24
spi:
- id: spi_tft
clk_pin: 18
mosi_pin: 23
miso_pin: 19
- id: spi_touch
clk_pin: 25
mosi_pin: 32
miso_pin: 34
display:
- platform: mipi_spi
model: st7796
spi_id: spi_tft
cs_pin: 5
dc_pin: 17
reset_pin: 16
rotation: 90
spi_mode: 3
color_order: bgr
pixel_mode: 16bit
auto_clear_enabled: true
lambda: |-
it.fill(Color(0, 0, 0));
// ---- CORNER COLOR BOXES ----
it.filled_rectangle(0, 0, 40, 40, Color(255, 0, 0)); // RED TL
it.filled_rectangle(it.get_width()-40, 0, 40, 40, Color(0, 255, 0)); // GREEN TR
it.filled_rectangle(0, it.get_height()-40, 40, 40, Color(0, 0, 255)); // BLUE BL
it.filled_rectangle(it.get_width()-40, it.get_height()-40, 40, 40, Color(255,255,0)); // YELLOW BR
// ---- ARROWS + LABELS ----
int cx = it.get_width() / 2;
int cy = it.get_height() / 2;
// UP ↑
it.line(cx, cy, cx, cy-60, Color(255,255,255));
it.line(cx, cy-60, cx-10, cy-50, Color(255,255,255));
it.line(cx, cy-60, cx+10, cy-50, Color(255,255,255));
it.printf(cx, cy-80, id(default_font), Color(255,255,255), TextAlign::CENTER, "UP");
// DOWN ↓
it.line(cx, cy, cx, cy+60, Color(255,255,255));
it.line(cx, cy+60, cx-10, cy+50, Color(255,255,255));
it.line(cx, cy+60, cx+10, cy+50, Color(255,255,255));
it.printf(cx, cy+80, id(default_font), Color(255,255,255), TextAlign::CENTER, "DOWN");
// LEFT ←
it.line(cx, cy, cx-60, cy, Color(255,255,255));
it.line(cx-60, cy, cx-50, cy-10, Color(255,255,255));
it.line(cx-60, cy, cx-50, cy+10, Color(255,255,255));
it.printf(cx-90, cy, id(default_font), Color(255,255,255), TextAlign::CENTER, "LEFT");
// RIGHT →
it.line(cx, cy, cx+60, cy, Color(255,255,255));
it.line(cx+60, cy, cx+50, cy-10, Color(255,255,255));
it.line(cx+60, cy, cx+50, cy+10, Color(255,255,255));
it.printf(cx+90, cy, id(default_font), Color(255,255,255), TextAlign::CENTER, "RIGHT");
// ---- Touch readout as text ----
if(id(last_color) != "") {
it.printf(cx, cy+120, id(default_font), Color(255,255,255), TextAlign::CENTER, "%s", id(last_color).c_str());
}
output:
- platform: ledc
id: tft_bl
pin: 21
frequency: 20000 Hz
light:
- platform: monochromatic
id: backlight
output: tft_bl
restore_mode: ALWAYS_ON
text_sensor:
- platform: template
name: "Touch Raw X"
id: raw_x
update_interval: never
- platform: template
name: "Touch Raw Y"
id: raw_y
update_interval: never
sensor:
- platform: template
name: "Touch Raw X Numeric"
id: raw_x_sensor
update_interval: 50ms
lambda: |-
return id(raw_x).state != "" ? atof(id(raw_x).state.c_str()) : 0;
- platform: template
name: "Touch Raw Y Numeric"
id: raw_y_sensor
update_interval: 50ms
lambda: |-
return id(raw_y).state != "" ? atof(id(raw_y).state.c_str()) : 0;
touchscreen:
- platform: xpt2046
id: ts
spi_id: spi_touch
cs_pin: 22
calibration:
x_min: 300
x_max: 3900
y_min: 300
y_max: 3900
transform:
swap_xy: true
on_touch:
then:
- lambda: |-
int X = touch.x_raw;
int Y = touch.y_raw;
// 🔥 FIXED MAPPING — TR <-> BL swap corrected
if (X > 3300 && Y > 3300) id(last_color) = "RED (TL)";
else if (X < 900 && Y > 3300) id(last_color) = "GREEN (TR)"; // swapped here
else if (X > 3300 && Y < 900) id(last_color) = "BLUE (BL)"; // swapped here
else if (X < 900 && Y < 900) id(last_color) = "YELLOW (BR)";
else id(last_color) = "CENTER";
Once on the WROOM, and I think it’s kind of a must for it to be a WROOM from what I understood from the Github post, you should get this on the screen:
Touch functionality can be tested by clicking on the colored squares. When you click on a square, it should write on the screen the color of the square and it’s location (TL - top left). Exact touch coordinates from the screen can be seen in logs.
Hope this helps someone out and avoids the hours and hours of LLM prompting to get this running.

