ESPHome 2.9″ Waveshare ePaper (296×128): Clean PV Flow Layout

I’m using a 2.9″ Waveshare ePaper (296×128) on an ESP8266 D1 Mini Pro with ESPHome, pulling all values from Home Assistant
– PV power (sensor.pv_gesamt)
– house consumption (sensor.strom_eigengebrauch)
– grid import/export (sensor.s10x_consumption_from_grid, sensor.s10x_export_to_grid)
– battery charge/discharge + SOC (sensor.s10x_battery_charge, sensor.s10x_battery_discharge, sensor.s10x_state_of_charge).

esp8266:
  board: d1_mini_pro

Script:

captive_portal:

font:
  - file: "gfonts://Roboto+Mono@300"
    id: font_value
    size: 16

  - file: "gfonts://Roboto+Mono@300"
    id: font_home
    size: 26

  - file: "gfonts://Roboto+Mono@300"
    id: font_arrow
    size: 16

  - file: "gfonts://Material+Symbols+Outlined"
    id: icons_28
    size: 28
    glyphs:
      [
        "\U0000e88a", # home
        "\U0000ec0f", # sun
        "\U0000f102", # grid/power (Versorger-Ersatz)
        "\U0000e1a4", # battery 100
        "\U0000ebd2",
        "\U0000ebd4",
        "\U0000ebe2",
        "\U0000ebdd",
        "\U0000ebe0",
        "\U0000ebd9",
        "\U0000ebdc"  # battery 0
      ]

  - file: "gfonts://Material+Symbols+Outlined"
    id: icons_52
    size: 52
    glyphs: ["\U0000e88a"]

spi:
  clk_pin: D0
  mosi_pin: D1

display:
  - platform: waveshare_epaper
    cs_pin: D2
    dc_pin: D3
    busy_pin: D4
    reset_pin: D5
    model: 2.90inv2-r2
    rotation: 90
    reset_duration: 2ms
    update_interval: 15s

    lambda: |-
      const float TH = 0.02f; // 20W (in kW)

      const float pv_kw    = id(pv_gesamt).state;
      const float home_kw  = id(stromverbrauch).state;

      const float grid_in  = id(netzbezug).state;
      const float grid_out = id(einspeisung).state;
      const float grid_net = grid_in - grid_out;

      const float bat_in   = id(inbatterie).state;
      const float bat_out  = id(outbatterie).state;
      const float bat_net  = bat_out - bat_in;

      const float soc      = id(batterieload).state;

      auto fmt_kw = [&](float kw) -> std::string {
        if (isnan(kw)) return std::string("--");
        float w = kw * 1000.0f;
        char buf[18];
        if (fabsf(w) < 1000.0f) snprintf(buf, sizeof(buf), "%.0fW", w);
        else                    snprintf(buf, sizeof(buf), "%.2fkW", kw);
        return std::string(buf);
      };

      auto dotted_h = [&](int x1, int y, int x2, int step, int on) {
        if (x2 < x1) { int t=x1; x1=x2; x2=t; }
        for (int x = x1; x <= x2; x += step) {
          it.line(x, y, std::min(x + on, x2), y);
        }
      };

      auto arrow_right = [&](int x, int y) { it.filled_triangle(x, y, x-7, y-4, x-7, y+4); };
      auto arrow_left  = [&](int x, int y) { it.filled_triangle(x, y, x+7, y-4, x+7, y+4); };

      auto flow_h = [&](int x1, int y, int x2, bool head_at_end) {
        dotted_h(x1, y, x2, 8, 3);
        if (head_at_end) {
          if (x2 > x1) arrow_right(x2, y);
          else         arrow_left(x2, y);
        } else {
          if (x2 > x1) arrow_left(x1, y);
          else         arrow_right(x1, y);
        }
      };

      auto midx = [&](int a, int b) -> int { return a + (b - a) / 2; };

      // Batterie-Icon nach SOC
      std::string battery_icon = "\U0000ebdc";
      if (soc >= 95.0)      battery_icon = "\U0000e1a4";
      else if (soc >= 85.0) battery_icon = "\U0000ebd2";
      else if (soc >= 70.0) battery_icon = "\U0000ebd4";
      else if (soc >= 55.0) battery_icon = "\U0000ebe2";
      else if (soc >= 40.0) battery_icon = "\U0000ebdd";
      else if (soc >= 25.0) battery_icon = "\U0000ebe0";
      else if (soc >= 10.0) battery_icon = "\U0000ebd9";

      // ===== Layout (296x128) =====
      const int HOME_X  = 120;
      const int HOME_Y  = 28;
      const int HOME_CX = 150;

      // Pfeil-Y Positionen
      const int Y_PV   = 32;
      const int Y_GRID = 56;
      const int Y_BAT  = 92;

      // Icons
      const int PV_ICON_X  = 6;
      const int PV_ICON_Y  = 4;

      const int BAT_ICON_X = 6;
      const int BAT_ICON_Y = 74;
      const int BAT_CX     = BAT_ICON_X + 14;

      const int GRID_ICON_X = 268;
      const int GRID_ICON_Y = 44;

      const int X_HOME_LEFT  = HOME_CX - 18;
      const int X_HOME_RIGHT = HOME_CX + 28;

      const int L = 84;
      const int X_LEFT_FLOW  = X_HOME_LEFT - L;
      const int X_RIGHT_FLOW = X_HOME_RIGHT + L;

      const int LABEL_DY = 20;   // <-- war 16

      // ===== Zeichnen =====
      it.printf(PV_ICON_X, PV_ICON_Y, id(icons_28), "\U0000ec0f");

      it.printf(BAT_ICON_X, BAT_ICON_Y, id(icons_28), "%s", battery_icon.c_str());
      char soc_buf[8];
      snprintf(soc_buf, sizeof(soc_buf), "%.0f%%", soc);
      it.printf(BAT_CX, BAT_ICON_Y + 30, id(font_value), TextAlign::TOP_CENTER, "%s", soc_buf);

      it.printf(HOME_X, HOME_Y, id(icons_52), "\U0000e88a");

      it.printf(HOME_CX, 88, id(font_home), TextAlign::TOP_CENTER, "%s", fmt_kw(home_kw).c_str());

      it.printf(GRID_ICON_X, GRID_ICON_Y, id(icons_28), "\U0000f102");

      // PV -> Home
      if (!isnan(pv_kw) && pv_kw > TH) flow_h(X_LEFT_FLOW, Y_PV, X_HOME_LEFT, true);
      else                             dotted_h(X_LEFT_FLOW, Y_PV, X_HOME_LEFT, 8, 3);

      float pv_show = (!isnan(pv_kw) && pv_kw > TH) ? pv_kw : 0.0f;
      it.printf(midx(X_LEFT_FLOW, X_HOME_LEFT), Y_PV - LABEL_DY, id(font_arrow),
                TextAlign::TOP_CENTER, "%s", fmt_kw(pv_show).c_str());

      // Akku <-> Home
      bool bat_active = (!isnan(bat_net) && fabsf(bat_net) > TH);
      if (bat_active) {
        if (bat_net > 0) flow_h(X_LEFT_FLOW, Y_BAT, X_HOME_LEFT, true);
        else             flow_h(X_HOME_LEFT, Y_BAT, X_LEFT_FLOW, true);
      } else {
        dotted_h(X_LEFT_FLOW, Y_BAT, X_HOME_LEFT, 8, 3);
      }

      float bat_show = (bat_active) ? fabsf(bat_net) : 0.0f;
      it.printf(midx(X_LEFT_FLOW, X_HOME_LEFT), Y_BAT - LABEL_DY, id(font_arrow),
                TextAlign::TOP_CENTER, "%s", fmt_kw(bat_show).c_str());

      // Netz <-> Home
      bool grid_active = (!isnan(grid_net) && fabsf(grid_net) > TH);
      if (grid_active) {
        if (grid_net > 0) flow_h(X_RIGHT_FLOW, Y_GRID, X_HOME_RIGHT, true);
        else              flow_h(X_HOME_RIGHT, Y_GRID, X_RIGHT_FLOW, true);
      } else {
        dotted_h(X_HOME_RIGHT, Y_GRID, X_RIGHT_FLOW, 8, 3);
      }

      float grid_show = (grid_active) ? fabsf(grid_net) : 0.0f;
      it.printf(midx(X_HOME_RIGHT, X_RIGHT_FLOW), Y_GRID - LABEL_DY, id(font_arrow),
                TextAlign::TOP_CENTER, "%s", fmt_kw(grid_show).c_str());

sensor:
  - platform: homeassistant
    id: stromverbrauch
    entity_id: sensor.strom_eigengebrauch
  - platform: homeassistant
    id: pv_gesamt
    entity_id: sensor.pv_gesamt
  - platform: homeassistant
    id: netzbezug
    entity_id: sensor.s10x_consumption_from_grid
  - platform: homeassistant
    id: einspeisung
    entity_id: sensor.s10x_export_to_grid
  - platform: homeassistant
    id: inbatterie
    entity_id: sensor.s10x_battery_charge
  - platform: homeassistant
    id: outbatterie
    entity_id: sensor.s10x_battery_discharge
  - platform: homeassistant
    id: batterieload
    entity_id: sensor.s10x_state_of_charge

web_server:
  port: 80Code-Sprache: PHP (php)

Preview: