Add epaper-climate-display.yaml

This commit is contained in:
2026-08-16 13:24:07 +02:00
commit 74002f6004
+666
View File
@@ -0,0 +1,666 @@
# ----------------------------------------------------------------------------------
# epaper-climate-display (ESPHome / Wemos D1 mini ESP8266 + GoodDisplay GDEY042T81)
#
# FIXED VERSION - Using LOCAL fonts to avoid compilation memory issues
# Font files located at:
# - /config/esphome/fonts/roboto/Roboto_Condensed-Regular.ttf
# - /config/esphome/fonts/roboto/Roboto_Condensed-Bold.ttf
# ----------------------------------------------------------------------------------
esphome:
name: epaper-climate-display
friendly_name: Epaper Climate Display
on_boot:
priority: 250
then:
- lambda: |-
// Boot gather (data) up to 60s
id(boot_gather_active) = true;
id(boot_gather_deadline_ms) = millis() + 60000; // 60 seconds
id(did_first_draw) = false;
// Simple boot screen shown up to 30s (or until first main draw)
id(show_boot_screen) = true;
id(boot_screen_deadline_ms) = millis() + 30000; // 30 seconds
- script.execute: boot_gather_watchdog
# Draw boot screen immediately
- component.update: epaper # Draw "Loading" screen
- delay: 200ms
- script.execute: backlight_delay # PULSE: "I'm alive and looking for WiFi/Data"
esp8266:
board: d1_mini
logger:
baud_rate: 0 # This stops the ESP8266 from "talking" over your LED pin (default option is without this line)
api:
encryption:
key: "/XNy5H5k3/hV0E8xpgFQhY2pQYMo25Y8E8Jt+9uaTgA="
reboot_timeout: 0s
ota:
- platform: esphome
password: "88fcca1cd8e29c78697cfcee032c8e1e"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
min_auth_mode: WPA2
fast_connect: true
power_save_mode: none
ap:
ssid: "Epaper-Climate-Display"
password: "QCaF9KQtPXUn"
captive_portal:
time:
- platform: homeassistant
id: ha_time
# --- LIGHT / PWM CONFIGURATION ---
output:
- platform: esp8266_pwm
id: backlight_pwm
pin: GPIO1 # TX Pin
frequency: 500 Hz
light:
- platform: monochromatic
id: backlight
name: "Epaper Backlight"
output: backlight_pwm
default_transition_length: 1s # All transitions take 1s by default
restore_mode: ALWAYS_OFF
# We don't use display.update_interval; we schedule refreshes via interval with quiet-hours logic.
interval:
- interval: 10min
then:
- lambda: |-
// 1. SETTINGS: Enter your pause times here
const int start_h = 23;
const int start_m = 59;
const int end_h = 4;
const int end_m = 59;
// 2. Check if time is valid
if (!id(ha_time).now().is_valid()) {
id(epaper).update();
return;
}
auto now = id(ha_time).now();
// 3. Automatic Calculations
int current_minutes = (now.hour * 60) + now.minute;
int start_pause_res = (start_h * 60) + start_m;
int end_pause_res = (end_h * 60) + end_m;
// 4. Logic check (handles midnight rollover)
bool is_paused = false;
if (start_pause_res > end_pause_res) {
// Range crosses midnight (e.g., 23:30 to 05:45)
if (current_minutes >= start_pause_res || current_minutes < end_pause_res) {
is_paused = true;
}
} else {
// Range is within one day (e.g., 13:00 to 15:00)
if (current_minutes >= start_pause_res && current_minutes < end_pause_res) {
is_paused = true;
}
}
if (is_paused) {
ESP_LOGD("display", "Scheduled refresh paused (Current: %02d:%02d)", now.hour, now.minute);
return;
}
// Trigger the E-paper update and light indication
id(epaper).update(); // 1. Refresh (Blocking)
id(refresh_indication).execute(); // 2. Pulse Light
globals:
# Boot gather controls
- id: boot_gather_active
type: bool
restore_value: no
initial_value: 'true'
- id: boot_gather_deadline_ms
type: uint32_t
restore_value: no
initial_value: '0'
- id: did_first_draw
type: bool
restore_value: no
initial_value: 'false'
# Boot screen controls (simple, no animation)
- id: show_boot_screen
type: bool
restore_value: no
initial_value: 'true'
- id: boot_screen_deadline_ms
type: uint32_t
restore_value: no
initial_value: '0'
# Cached values: room temps/humidity
- id: balcony_temp_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: balcony_hum_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: living_temp_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: living_hum_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: bathroom_temp_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: bathroom_hum_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: bedroom_temp_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: bedroom_hum_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: room_temp_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: room_hum_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: kitchen_temp_cache
type: float
restore_value: no
initial_value: 'NAN'
- id: kitchen_hum_cache
type: float
restore_value: no
initial_value: 'NAN'
# Cached footer sensors
- id: pressure_cache_mbar
type: float
restore_value: no
initial_value: 'NAN'
- id: battery_cache_pct
type: float
restore_value: no
initial_value: 'NAN'
- id: daily_energy_cache_kwh
type: float
restore_value: no
initial_value: 'NAN'
# Cached solar sensors
- id: daily_solar_cache_wh
type: float
restore_value: no
initial_value: 'NAN'
- id: daily_rsolar_cache_kwh
type: float
restore_value: no
initial_value: 'NAN'
script:
# Wait for values up to 60s, then do the FIRST draw once.
- id: boot_gather_watchdog
mode: restart
then:
- while:
condition:
lambda: 'return id(boot_gather_active);'
then:
- delay: 500ms
- lambda: |-
auto have = [](float v){ return !isnan(v); };
// Require all room sensors + footer sensors + new solar before "all_ok"
const bool all_ok =
have(id(balcony_temp_cache)) && have(id(balcony_hum_cache)) &&
have(id(living_temp_cache)) && have(id(living_hum_cache)) &&
have(id(bathroom_temp_cache)) && have(id(bathroom_hum_cache)) &&
have(id(bedroom_temp_cache)) && have(id(bedroom_hum_cache)) &&
have(id(room_temp_cache)) && have(id(room_hum_cache)) &&
have(id(kitchen_temp_cache)) && have(id(kitchen_hum_cache)) &&
have(id(pressure_cache_mbar)) &&
have(id(battery_cache_pct)) &&
have(id(daily_energy_cache_kwh)) &&
have(id(daily_solar_cache_wh)) &&
have(id(daily_rsolar_cache_kwh));
const bool timed_out = (millis() > id(boot_gather_deadline_ms));
if ((all_ok || timed_out) && !id(did_first_draw)) {
id(did_first_draw) = true;
id(boot_gather_active) = false;
// Stop showing boot screen once we switch to main screen.
id(show_boot_screen) = false;
// First draw now that we have data (or timed out)
id(epaper).update(); // Draw main screen
id(backlight_delay).execute(); // PULSE: "Data received, update complete"
}
# Backlight: turn ON for 10 seconds then OFF; restart extends the ON time.
- id: backlight_delay
mode: restart
then:
- light.turn_on:
id: backlight
brightness: 100%
transition_length: 500ms # Fast fade in on 500ms
- delay: 10s
- light.turn_off:
id: backlight
transition_length: 1500ms # Fades out over 2s
- id: refresh_indication
mode: restart
then:
# Simple 5-second 25% pulse
- light.turn_on:
id: backlight
brightness: 35%
transition_length: 500ms
- delay: 3s
- light.turn_off:
id: backlight
transition_length: 500ms
binary_sensor:
# Touch refresh button: force refresh anytime (including quiet hours)
- platform: gpio
id: touch_refresh
name: "Touch Refresh"
pin:
number: GPIO12 # D6
mode:
input: true
pullup: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
on_press:
then:
- lambda: |-
ESP_LOGI("touch", "Manual refresh requested");
- component.update: epaper # 1. Refresh the screen (Blocking)
- script.execute: refresh_indication # 2. Pulse the light (Starts after screen is done)
# Touch light button: start backlight timer (no toggle)
- platform: gpio
id: touch_light
name: "Touch Light"
pin:
number: GPIO3 # RX
mode:
input: true
pullup: true
filters:
- delayed_on: 20ms
- delayed_off: 20ms
on_press:
then:
- lambda: |-
ESP_LOGI("touch", "Backlight requested (preset seconds)");
- script.execute: backlight_delay
sensor:
# --- Room sensors (Home Assistant) ---
- platform: homeassistant
id: balcony_temp
entity_id: sensor.indoor_outdoor_meter_f7c8_temperatura
on_value: { then: [ lambda: 'id(balcony_temp_cache) = x;' ] }
- platform: homeassistant
id: balcony_hum
entity_id: sensor.indoor_outdoor_meter_f7c8_vlaznost
on_value: { then: [ lambda: 'id(balcony_hum_cache) = x;' ] }
- platform: homeassistant
id: living_temp
entity_id: sensor.indoor_outdoor_meter_f965_temperatura
on_value: { then: [ lambda: 'id(living_temp_cache) = x;' ] }
- platform: homeassistant
id: living_hum
entity_id: sensor.indoor_outdoor_meter_f965_vlaznost
on_value: { then: [ lambda: 'id(living_hum_cache) = x;' ] }
- platform: homeassistant
id: bathroom_temp
entity_id: sensor.meter_plus_3550_temperatura
on_value: { then: [ lambda: 'id(bathroom_temp_cache) = x;' ] }
- platform: homeassistant
id: bathroom_hum
entity_id: sensor.meter_plus_3550_vlaznost
on_value: { then: [ lambda: 'id(bathroom_hum_cache) = x;' ] }
- platform: homeassistant
id: bedroom_temp
entity_id: sensor.meter_plus_080d_temperatura
on_value: { then: [ lambda: 'id(bedroom_temp_cache) = x;' ] }
- platform: homeassistant
id: bedroom_hum
entity_id: sensor.meter_plus_080d_vlaznost
on_value: { then: [ lambda: 'id(bedroom_hum_cache) = x;' ] }
- platform: homeassistant
id: room_temp
entity_id: sensor.meter_plus_f010_temperatura
on_value: { then: [ lambda: 'id(room_temp_cache) = x;' ] }
- platform: homeassistant
id: room_hum
entity_id: sensor.meter_plus_f010_vlaznost
on_value: { then: [ lambda: 'id(room_hum_cache) = x;' ] }
- platform: homeassistant
id: kitchen_temp
entity_id: sensor.meter_plus_86aa_temperatura
on_value: { then: [ lambda: 'id(kitchen_temp_cache) = x;' ] }
- platform: homeassistant
id: kitchen_hum
entity_id: sensor.meter_plus_86aa_vlaznost
on_value: { then: [ lambda: 'id(kitchen_hum_cache) = x;' ] }
# --- Footer sensors (Home Assistant) ---
- platform: homeassistant
id: pressure_mbar
entity_id: sensor.arso_weather_brezice_zracni_tlak
on_value:
then:
- lambda: id(pressure_cache_mbar) = x;
- platform: homeassistant
id: battery_pct
entity_id: sensor.delta_2_max_0110_battery_level
on_value:
then:
- lambda: id(battery_cache_pct) = x;
- platform: homeassistant
id: daily_energy_kwh
entity_id: sensor.daily_household_power_usage
on_value:
then:
- lambda: id(daily_energy_cache_kwh) = x;
# --- Solar sensor ---
- platform: homeassistant
id: daily_solar_wh
entity_id: sensor.ecoflow_daily_solar_power
on_value:
then:
- lambda: id(daily_solar_cache_wh) = x;
# --- Solar sensor (new remote panel forecast) ---
- platform: homeassistant
id: daily_rsolar_kwh
entity_id: sensor.solcast_sc_daily_measurment_no_negative_eprices
on_value:
then:
- lambda: 'id(daily_rsolar_cache_kwh) = x;'
spi:
clk_pin: GPIO14
mosi_pin: GPIO13
# Fonts using LOCAL Roboto Condensed files
font:
- file: fonts/roboto/Roboto_Condensed-Regular.ttf
id: font_title
size: 20
glyphs:
- "! 0123456789" # space and numbers
- "ABCDEFGHIJKLMNOPQRSTUVWXYZČŠŽ" # uppercase
- "abcdefghijklmnopqrstuvwxyzčšž" # lowercase
- '.,-:;()' # punctuation
- file: fonts/roboto/Roboto_Condensed-Bold.ttf
id: font_temp
size: 42
glyphs:
- ' ' # space
- '0123456789,.-'
- "ABCDEFGHIJKLMNOPQRSTUVWXYZČŠŽ"
- "abcdefghijklmnopqrstuvwxyzčšž"
- file: fonts/roboto/Roboto_Condensed-Bold.ttf
id: font_hum
size: 30
glyphs:
- ' ' # space
- '0123456789,.'
- "ABCDEFGHIJKLMNOPQRSTUVWXYZČŠŽ"
- "abcdefghijklmnopqrstuvwxyzčšž"
- '%'
- file: fonts/roboto/Roboto_Condensed-Regular.ttf
id: font_unit
size: 22
glyphs:
- ' C%'
- "\u00B0" # degree symbol (°)
- file: fonts/roboto/Roboto_Condensed-Regular.ttf
id: font_footer
size: 18
glyphs:
- '0123456789'
- "ABCDEFGHIJKLMNOPQRSTUVWXYZČŠŽ"
- "abcdefghijklmnopqrstuvwxyzčšž"
- ' .,/:;()-%|>'
# Material Design Icons
- file: fonts/materialdesignicons-webfont.ttf
id: font_mdi
size: 18
glyphs:
- "\U000F050F" # mdi-thermometer
- "\U000F0080" # mdi-battery
- "\U000F140B" # mdi-lightning-bolt
- "\U000F1A73" # mdi-solar-panel
- "\U000F0D9B" # mdi-solar-power-variant (NEW)
- file: fonts/materialdesignicons-webfont.ttf
id: font_boot
size: 40
glyphs:
- "\U000F050F" # mdi-thermometer (temperature)
- "\U000F0080" # mdi-battery (battery)
- "\U000F140B" # mdi-lightning-bolt (energy)
- "\U000F1A73" # mdi-solar-panel (solar)
- "\U000F0D9B" # mdi-solar-power-variant (NEW)
display:
- platform: waveshare_epaper
id: epaper
model: gdey042t81
cs_pin: GPIO15
dc_pin: GPIO4
reset_pin: GPIO5
busy_pin: GPIO16
update_interval: never
full_update_every: 1
rotation: 0
lambda: |-
const int w = it.get_width(); // 400
const int h = it.get_height(); // 300
// --- BOOT SCREEN (simple, no animation) ---
if (id(show_boot_screen) && (millis() <= id(boot_screen_deadline_ms)) && !id(did_first_draw)) {
it.fill(COLOR_OFF);
it.printf(w/2, 50, id(font_boot), TextAlign::TOP_CENTER, "\U000F1A73\U000F140B\U000F0080\U000F050F");
it.printf(w/2, 110, id(font_temp), TextAlign::TOP_CENTER, "HA E-ZASLON");
it.printf(w/2, 190, id(font_hum), TextAlign::TOP_CENTER, "Pridobivam podatke");
it.printf(w/2, 230, id(font_title), TextAlign::TOP_CENTER, "To bo trajalo nekaj sekund");
return;
}
// --- MAIN GRID SCREEN ---
const int margin = 8;
const int gap = 6;
const int footer_line_h = 20;
const int footer_h = footer_line_h * 2 + 6;
const int grid_top = margin;
const int grid_bottom = h - footer_h - margin;
const int cols = 3;
const int rows = 2;
const int grid_h = grid_bottom - grid_top;
const int cell_w = (w - 2*margin - (cols-1)*gap) / cols;
const int cell_h = (grid_h - (rows-1)*gap) / rows;
auto cell_x = [&](int c) { return margin + c * (cell_w + gap); };
auto cell_y = [&](int r) { return grid_top + r * (cell_h + gap); };
auto fmt_temp = [&](float v) -> std::string {
if (isnan(v)) return std::string("--,-");
char buf[16];
snprintf(buf, sizeof(buf), "%.1f", v);
std::string out(buf);
for (auto &ch : out) if (ch == '.') ch = ',';
return out;
};
auto fmt_hum = [&](float v) -> std::string {
if (isnan(v)) return std::string("--");
char buf[16];
snprintf(buf, sizeof(buf), "%.0f", v);
return std::string(buf);
};
auto fmt_pressure = [&](float v) -> std::string {
if (isnan(v)) return std::string("--");
char buf[20];
snprintf(buf, sizeof(buf), "%.0f mbar", v);
return std::string(buf);
};
auto fmt_battery = [&](float v) -> std::string {
if (isnan(v)) return std::string("--");
char buf[20];
snprintf(buf, sizeof(buf), "%.0f%%", v);
return std::string(buf);
};
auto fmt_energy = [&](float v) -> std::string {
if (isnan(v)) return std::string("--");
char buf[24];
snprintf(buf, sizeof(buf), "%.2f kWh", v);
return std::string(buf);
};
auto fmt_solar = [&](float v) -> std::string {
if (isnan(v)) return std::string("--");
char buf[24];
snprintf(buf, sizeof(buf), "%.0f Wh", v);
return std::string(buf);
};
auto fmt_rsolar = [&](float v) -> std::string {
if (isnan(v)) return std::string("--");
char buf[24];
snprintf(buf, sizeof(buf), "%.2f kWh", v);
return std::string(buf);
};
it.fill(COLOR_OFF);
int v1 = cell_x(1) - gap/2;
int v2 = cell_x(2) - gap/2;
it.line(v1, grid_top, v1, grid_bottom, COLOR_ON);
it.line(v2, grid_top, v2, grid_bottom, COLOR_ON);
int h1 = cell_y(1) - gap/2;
it.line(margin, h1, w - margin, h1, COLOR_ON);
auto draw_cell = [&](int c, int r, const char* name, float t_cache, float h_cache) {
int x = cell_x(c);
int y = cell_y(r);
it.filled_rectangle(x, y, cell_w, cell_h, COLOR_OFF);
it.printf(x + 10, y + 3, id(font_title), TextAlign::TOP_LEFT, "%s", name);
const std::string t_str = fmt_temp(t_cache);
it.printf(x + 10, y + 22, id(font_temp), TextAlign::TOP_LEFT, "%s", t_str.c_str());
int tbx=0, tby=0, tbw=0, tbh=0;
it.get_text_bounds(0, 0, t_str.c_str(), id(font_temp), TextAlign::TOP_LEFT, &tbx, &tby, &tbw, &tbh);
it.printf(x + 10 + tbw + 3, y + 22 + 12, id(font_unit), TextAlign::TOP_LEFT, "%s", "\xC2\xB0" "C");
it.printf(x + 10, y + cell_h - 34, id(font_hum), TextAlign::TOP_LEFT, "%s%%", fmt_hum(h_cache).c_str());
};
draw_cell(0, 0, "Balkon", id(balcony_temp_cache), id(balcony_hum_cache));
draw_cell(1, 0, "Dnevna soba", id(living_temp_cache), id(living_hum_cache));
draw_cell(2, 0, "Kopalnica", id(bathroom_temp_cache), id(bathroom_hum_cache));
draw_cell(0, 1, "Spalnica", id(bedroom_temp_cache), id(bedroom_hum_cache));
draw_cell(1, 1, "Soba", id(room_temp_cache), id(room_hum_cache));
draw_cell(2, 1, "Kuhinja", id(kitchen_temp_cache), id(kitchen_hum_cache));
const int footer_y = h - footer_h;
it.filled_rectangle(0, footer_y, w, footer_h, COLOR_OFF);
const int line1_y = footer_y + 2;
const int line2_y = footer_y + footer_line_h + 4;
// Footer line 1
it.printf(margin + 0, line1_y, id(font_mdi), TextAlign::TOP_LEFT, "\U000F050F");
it.printf(margin + 26, line1_y + 1, id(font_footer), TextAlign::TOP_LEFT, "%s", fmt_pressure(id(pressure_cache_mbar)).c_str());
it.printf(margin + 127, line1_y, id(font_mdi), TextAlign::TOP_LEFT, "\U000F0080");
it.printf(margin + 153, line1_y + 1, id(font_footer), TextAlign::TOP_LEFT, "%s", fmt_battery(id(battery_cache_pct)).c_str());
it.printf(margin + 258, line1_y, id(font_mdi), TextAlign::TOP_LEFT, "\U000F140B");
it.printf(margin + 284, line1_y + 1, id(font_footer), TextAlign::TOP_LEFT, "%s", fmt_energy(id(daily_energy_cache_kwh)).c_str());
// Footer line 2
// Left: Solar Local
it.printf(margin + 0, line2_y, id(font_mdi), TextAlign::TOP_LEFT, "\U000F1A73");
it.printf(margin + 26, line2_y + 1, id(font_footer), TextAlign::TOP_LEFT, "%s", fmt_solar(id(daily_solar_cache_wh)).c_str());
// Center: Remote Solar panel forecast (New)
it.printf(margin + 127, line2_y, id(font_mdi), TextAlign::TOP_LEFT, "\U000F0D9B");
it.printf(margin + 153, line2_y + 1, id(font_footer), TextAlign::TOP_LEFT, "%s", fmt_rsolar(id(daily_rsolar_cache_kwh)).c_str());
// Right: Updated timestamp (Restored original style but with 2-digit year)
std::string updated = "Osveženo: --";
if (id(ha_time).now().is_valid()) {
auto now = id(ha_time).now();
char buf[80];
snprintf(buf, sizeof(buf), "%d. %d. %02d, %d:%02d",
now.day_of_month, now.month, (now.year % 100), now.hour, now.minute);
updated = std::string(buf);
}
it.printf(margin + 256, line2_y, id(font_footer), TextAlign::TOP_LEFT, "|");
it.printf(w - margin, line2_y + 1, id(font_footer), TextAlign::TOP_RIGHT, "%s", updated.c_str());