ESP32 DIY Energy Display with ESPHome

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

ComponentSDA PinSCL Pin
LCD DisplayGPIO21GPIO22
SCD41 SensorGPIO16GPIO17

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;