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:


