In this guide, you’ll learn how to set up an ESP32 board to display sensor data from Home Assistant and connected I²C sensors on a 20×4 LCD display. This setup is perfect for monitoring solar power production, indoor temperatures, CO₂ levels, and more — all in real-time!
What You’ll Need
- ESP32 board (e.g.
esp32dev) - 20×4 LCD with I²C backpack (PCF8574, address
0x27) - SCD41 sensor (CO₂, temperature, humidity) via I²C (address
0x62) - Access to Home Assistant (with the necessary sensors exposed via the API)
- ESPHome installed (via Home Assistant Add-On or CLI)
- A computer with USB for flashing the ESP32
Wiring
| Component | SDA Pin | SCL Pin |
|---|---|---|
| LCD Display | GPIO21 | GPIO22 |
| SCD41 Sensor | GPIO16 | GPIO17 |
Ensure pull-up resistors (typically 4.7kΩ) are present on SDA/SCL lines if your modules don’t include them.
ESPHome Configuration
Save the following as esp-pv-display.yaml:
YAML
esphome:
name: esp-pv-display
friendly_name: ESP-PV-Display
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: "XXXXXXXXXXXXXXXX"
ota:
- platform: esphome
password: "XXXXXXXXXXXXXXXX"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Esp-Pv-Display Fallback Hotspot"
password: "XXXXXXXXXXXXXXXX"
captive_portal:
i2c:
- id: bus_display
sda: 21
scl: 22
scan: true
- id: bus_scd41
sda: 16
scl: 17
scan: true
sensor:
- platform: homeassistant
id: strom_power
entity_id: sensor.sensor_shelly_pro3em_strom_total_active_power
- platform: homeassistant
id: bkw_power
entity_id: sensor.bkw_total_watt
- platform: homeassistant
id: bkw1_power
entity_id: sensor.bkw1_watt
- platform: homeassistant
id: bkw2_power
entity_id: sensor.bkw2_watt
- platform: homeassistant
id: bkw3_power
entity_id: sensor.bkw3_akku_solarleistung
- platform: homeassistant
id: bkw3_akku_ladestand
entity_id: sensor.bkw3_akku_ladestand
- platform: homeassistant
id: bkw3_akku_akkuleistung
entity_id: sensor.bkw3_akku_akkuleistung
- platform: homeassistant
id: bkw3_akku_ac_hausabgabe
entity_id: sensor.bkw3_akku_ac_hausabgabe
- platform: homeassistant
id: garten_temp
entity_id: sensor.sensor_th_outdoor_temperature
- platform: homeassistant
id: wohnz_temp
entity_id: sensor.temp_govee_wohn_8b_77_temperature
- platform: homeassistant
id: schlaf_temp
entity_id: sensor.temp_govee_schlaf_fa_60_temperature
- platform: homeassistant
id: terrasse_temp
entity_id: sensor.sensor_th_terrasse_temperature
- platform: wifi_signal
id: wifi_strength
update_interval: 30s
- platform: homeassistant
id: uv_index
entity_id: weather.forecast_home
attribute: uv_index
- platform: homeassistant
id: cloud_coverage
entity_id: weather.forecast_home
attribute: cloud_coverage
- platform: homeassistant
id: wind_speed
entity_id: weather.forecast_home
attribute: wind_speed
- platform: scd4x
i2c_id: bus_scd41
co2:
name: "CO2 Concentration"
id: scd41_co2
filters:
- lambda: |-
if (x > 2500) return 2500;
else return x;
- median:
window_size: 6
send_every: 1
send_first_at: 1
temperature:
name: "SCD41 Temperature"
id: scd41_temperature
humidity:
name: "SCD41 Humidity"
id: scd41_humidity
address: 0x62
update_interval: 60s
text_sensor:
- platform: homeassistant
id: nachste_abholung
entity_id: sensor.nachste_abholung
time:
- platform: homeassistant
id: home_time
display:
- platform: lcd_pcf8574
i2c_id: bus_display
dimensions: 20x4
address: 0x27
update_interval: 7s
lambda: |-
static int group = 0;
it.printf(18, 0, "%d", group);
it.printf(0, 0, "V %.0fW P %.0fW", id(strom_power).state, id(bkw_power).state);
it.printf(0, 1, "1 %.0fW 2 %.0fW 3 %.0fW", id(bkw1_power).state, id(bkw2_power).state, id(bkw3_power).state);
if (group == 2) {
if (id(nachste_abholung).has_state()) {
std::string pickup = id(nachste_abholung).state;
if (pickup.find("Morgen") != std::string::npos || pickup.find("Heute") != std::string::npos) {
it.print(0, 2, pickup.c_str());
} else {
group = 3;
}
} else {
group = 3;
}
}
switch(group) {
case 0:
it.printf(0, 2, "T.Wohn %.1fC", id(wohnz_temp).state);
it.printf(0, 3, "T.Schlaf %.1fC", id(schlaf_temp).state);
break;
case 1:
it.printf(0, 2, "T.Garten %.1fC", id(garten_temp).state);
it.printf(0, 3, "T.Terr. %.1fC", id(terrasse_temp).state);
break;
case 3:
it.printf(0, 2, "UV %.1f Cloud %.0f%%", id(uv_index).state, id(cloud_coverage).state);
it.printf(0, 3, "Wind %.1f km/h", id(wind_speed).state);
break;
case 4:
it.printf(0, 2, "WiFi %.0f", id(wifi_strength).state);
if (id(home_time).now().is_valid()) {
it.strftime(0, 3, "%a %d.%m. %H:%M", id(home_time).now());
} else {
it.print(0, 3, "--.-- --:--");
}
break;
case 5:
it.printf(0, 2, "Akku %.0f%%", id(bkw3_akku_ladestand).state);
if (id(bkw3_akku_akkuleistung).state > 0) {
it.printf(0, 3, "Laden %.0fW", id(bkw3_akku_akkuleistung).state);
} else {
it.printf(0, 3, "Entladen %.0fW", fabs(id(bkw3_akku_akkuleistung).state));
}
break;
case 6:
it.printf(0, 2, "CO2 %.0f ppm", id(scd41_co2).state);
it.printf(0, 3, "Hum %.1f%% T %.1fC", id(scd41_humidity).state, id(scd41_temperature).state);
break;
}
group = (group + 1) % 7;