This design features a 15° tilted display for optimal viewing angles and easier touchscreen interaction, making it perfect for desktop placement. This enclosure houses a complete air quality monitoring system with multiple sensors and an ILI9341 touchscreen display. The angled design ensures comfortable touch interaction while displaying real-time data including temperature, CO₂ levels, and particulate matter (PM1.0, PM2.5, PM10).
Watch project video
Key Features
- 15° tilt angle for ergonomic touchscreen operation
- Optimized airflow design with dedicated sensor chambers
- MCP9808 temperature sensor in isolated 45° angled air channel (top left)
- PMS7003 particulate sensor with side air intake/exhaust (right side)
- MH-Z19B NDIR CO₂ sensor centrally mounted (gold-colored module)
- 2.8" ILI9341 touchscreen (240x320 SPI)
- Clean, professional appearance suitable for home or office
- ESPHome compatible
Components Required
- ESP32 development board
- ILI9341 2.8" TFT touchscreen with XPT2046 controller
- MCP9808 temperature sensor
- MH-Z19B CO₂ sensor
- PMS7003 particulate matter sensor
- 2.8" TFT SPI display board (240x320 V1.2)
- M3 x 4mm screws (4 pcs)
Fusion drawing, 3D printable objects and example ESPHome code
Feel free to remix and share your improvements.
Example code for the ESPHome (some is in Finnish but you figure it out):
# SPI-väylä (SPI for display and touchscreen)
spi:
- id: spi_display
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
- id: spi_touch
clk_pin: GPIO14
mosi_pin: GPIO13
miso_pin: GPIO35
# I2C-väylä (I2C for the MCP or BME-xxx)
i2c:
sda: GPIO21
scl: GPIO22
scan: true
uart:
- id: uart_co2
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 9600
- id: uart_pm
tx_pin: GPIO32
rx_pin: GPIO33
baud_rate: 9600
# Kellonaika Home Assistantista (time from Home Assistant)
time:
- platform: homeassistant
id: ha_time
# Home Assistant -kytkimet (Switches for the HA)
switch:
- platform: homeassistant
id: tv_switch
entity_id: switch.xxx_televisio
- platform: homeassistant
id: etupiha_switch
entity_id: switch.xxx_valojenohjaus
- platform: homeassistant
id: olohuone_switch
entity_id: switch.xxx_ledinauha
- platform: homeassistant
id: takapiha_switch
entity_id: switch.xxx_takapiha
sensor:
- platform: mcp9808
id: mcp_temp
address: 0x18
name: "Huoneen lämpötila"
update_interval: 10s
- platform: mhz19
co2:
id: co2_sensor
name: "CO2"
temperature:
id: mhz_temp
name: "CO2-lämpötila"
update_interval: 60s
uart_id: uart_co2
- platform: pmsx003
type: PMSX003
pm_1_0:
name: "PM1.0"
id: pm1_0
pm_2_5:
name: "PM2.5"
id: pm2_5
pm_10_0:
name: "PM10"
id: pm10
uart_id: uart_pm
update_interval: 30s
font:
- file: "gfonts://Roboto"
id: font_small
size: 20
glyphs:
- "!\"%()+=,-./:°µ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzÄÖÅäöå"
- file: "gfonts://Roboto"
id: font_large
size: 60
glyphs:
- "0123456789:"
display:
- platform: mipi_spi
id: tft_display
model: ILI9341
cs_pin: GPIO15
dc_pin: GPIO4
reset_pin: GPIO25
spi_id: spi_display
rotation: 180
buffer_size: "50%"
pages:
- id: idle_page
lambda: |-
it.fill(Color::BLACK);
auto now = id(ha_time).now();
it.printf(120, 80, id(font_large), Color::WHITE, TextAlign::CENTER, "%02d:%02d", now.hour, now.minute);
it.printf(120, 180, id(font_small), Color::WHITE, TextAlign::CENTER, "%02d.%02d.%04d", now.day_of_month, now.month, now.year);
const char* weekdays[] = {"Su", "Ma", "Ti", "Ke", "To", "Pe", "La"};
it.printf(120, 220, id(font_small), Color(200, 200, 200), TextAlign::CENTER, weekdays[now.day_of_week - 1]);
- id: active_page
lambda: |-
it.fill(Color::BLACK);
// Sensoridata (ennallaan)
if (id(mcp_temp).has_state()) {
it.printf(10, 10, id(font_small), Color(255, 255, 0), "Lämpö: %.1f C", id(mcp_temp).state);
}
if (id(co2_sensor).has_state()) {
it.printf(10, 40, id(font_small), Color(0, 255, 0), "CO2: %.0f ppm", id(co2_sensor).state);
}
if (id(pm2_5).has_state()) {
it.printf(10, 70, id(font_small), Color(0, 255, 0), "PM2.5: %.0f µg/m3", id(pm2_5).state);
}
// Apufunktio nappulan piirtämiseen tilan mukaan
auto draw_button = [&](int x, int y, const char* text, bool is_on) {
// Vihreä jos päällä, tummanharmaa jos pois
Color bg_color = is_on ? Color(0, 150, 0) : Color(60, 60, 60);
it.filled_rectangle(x, y, 110, 80, bg_color);
it.rectangle(x, y, 110, 80, Color::WHITE); // Valkoinen reunus selkeyden vuoksi
it.printf(x + 55, y + 40, id(font_small), Color::WHITE, TextAlign::CENTER, text);
};
// Piirretään nappulat ja tarkistetaan tila Home Assistantista!
draw_button(5, 110, "TV", id(tv_switch).state);
draw_button(125, 110, "Etupiha", id(etupiha_switch).state);
draw_button(5, 210, "Olohuone", id(olohuone_switch).state);
draw_button(125, 210, "Takapiha", id(takapiha_switch).state);
# Script aktiivisen sivun näyttämiseen ja automaattiseen palautumiseen
script:
- id: show_active_page_script
mode: restart
then:
- display.page.show: active_page
- delay: 10s
- display.page.show: idle_page
touchscreen:
- platform: xpt2046
id: touch_screen
cs_pin: GPIO5
interrupt_pin: GPIO26
spi_id: spi_touch
threshold: 400
calibration:
x_min: 20
x_max: 3860
y_min: 340
y_max: 3860
transform:
mirror_x: true # Kääntää X-akselin, jotta vasen=0 ja oikea=240
display: tft_display
on_touch:
then:
- lambda: |-
// TV
if (touch.x <= 115 && touch.y >= 110 && touch.y <= 190) {
id(tv_switch).toggle();
}
// Etupiha
else if (touch.x >= 125 && touch.y >= 110 && touch.y <= 190) {
id(etupiha_switch).toggle();
}
// Olohuone
else if (touch.x <= 115 && touch.y >= 210 && touch.y <= 290) {
id(olohuone_switch).toggle();
}
// Takapiha
else if (touch.x >= 125 && touch.y >= 210 && touch.y <= 290) {
id(takapiha_switch).toggle();
}
- script.execute: show_active_page_script
# Taustavalo
output:
- platform: ledc
pin: GPIO27
id: backlight_pwm
light:
- platform: monochromatic
output: backlight_pwm
id: backlight_light
name: "Näytön taustavalo"
default_transition_length: 0s
restore_mode: ALWAYS_ON
# Automaattinen päivä/yö-himmennys
interval:
- interval: 60s
then:
- lambda: |-
auto now = id(ha_time).now();
if (now.hour >= 7 && now.hour < 22) {
// Päivä: kirkas (100%)
auto call = id(backlight_light).turn_on();
call.set_brightness(1.0);
call.perform();
} else {
// Yö: himmeä (20%)
auto call = id(backlight_light).turn_on();
call.set_brightness(0.2);
call.perform();
}


