# ---------------------------------------------------------------------------------- # 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());