From 74002f6004477b07f1107ae6b5aaad28db77bcc8 Mon Sep 17 00:00:00 2001 From: Amir Date: Sun, 16 Aug 2026 13:24:07 +0200 Subject: [PATCH] Add epaper-climate-display.yaml --- epaper-climate-display.yaml | 666 ++++++++++++++++++++++++++++++++++++ 1 file changed, 666 insertions(+) create mode 100644 epaper-climate-display.yaml diff --git a/epaper-climate-display.yaml b/epaper-climate-display.yaml new file mode 100644 index 0000000..a3182cd --- /dev/null +++ b/epaper-climate-display.yaml @@ -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()); \ No newline at end of file