Esphome with epaper 7.5 inch

I am using an esp32 with a waveshare 7.5 inch epaper.
I am trying to display date, and weather info. I am getting an issue with timeouts and disconnection.
Any help is much appreciated. Here is the error logs:

[22:22:30][I][app:100]: ESPHome version 2025.2.0 compiled on Feb 22 2025, 22:19:33
[22:22:34][I][display:267]: Updating ePaper Calendar...
[22:22:34][I][display:271]: Received Day Name: Saturday
[22:22:34][I][display:272]: Received Day Number: 22
[22:22:34][I][display:273]: Received Month: February
[22:22:34][I][display:286]: Formatted Date: Saturday 22 February
[22:22:34][I][display:314]: Mapped Weather Icon: 󰖝
[22:22:34][I][waveshare_epaper:3563]: Power on the display and hat
[22:22:45][E][waveshare_epaper:3508]: Timeout while displaying image!
[22:22:56][E][waveshare_epaper:3508]: Timeout while displaying image!
[22:22:56][W][component:237]: Component esphome.coroutine took a long time for an operation (21455 ms).
[22:22:56][W][component:238]: Components should block for at most 30 ms.
[22:23:10][I][safe_mode:041]: Boot seems successful; resetting boot loop counter

This is my yaml file:

esphome:
  name: mehdicalendar
  min_version: 2025.2.0
  build_path: build/mehdicalendar

  # on_boot must be under 'esphome:', not 'esp32:'
  on_boot:
    
    then:
      - wait_until:
          condition:
            lambda: "return !id(current_day_number).state.empty() && !id(current_month).state.empty();"
          
      - script.execute: update_screen

esp32:
  board: esp32dev
  framework:
    type: arduino

wifi:
  ssid: "xx"
  password: "xxxx"
  ap:
    ssid: "xxx"
    password: "xxx"

logger:
  level: INFO

api:
  encryption:
    key: "xxxxx"
  services:
    - service: refresh_display
      then:
        - component.update: eink_display
        

ota:
  platform: esphome

script:
  - id: update_screen

    then:
      - component.update: eink_display

time:
  - platform: sntp
    id: sntp_time
    on_time:
      - seconds: 0
        minutes: 0
        hours: 1
        then:
          - script.execute: update_screen
      - seconds: 0
        minutes: 0
        hours: 8
        days_of_week: SAT-SUN
        then:
          - script.execute: update_screen
      - seconds: 0
        minutes: 0
        hours: 16
        then:
          - script.execute: update_screen

# --- UPDATED SPI PINS ---
spi:
  clk_pin: 18
  mosi_pin: 23
  
  # MISO is typically not needed for e-paper

font:
  - file: "fonts/GothamRnd-Book.ttf"
    id: book20
    size: 20
    glyphs: &font-glyphs
      ['!', '"', '%', '(', ')', '+', '=', ',', '-', '_', '.', ':', '°', ' ',
      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      'é', 'à', 'è', 'ç', 'ù', 'É', 'À', 'È', 'Ç', 'Ù']
  - file: "fonts/GothamRnd-Book.ttf"
    id: book25
    size: 25
  - file: "fonts/GothamRnd-Bold.ttf"
    id: bold20
    size: 20
  - file: "fonts/GothamRnd-Bold.ttf"
    id: bold25
    size: 25
  - file: "fonts/GothamRnd-Bold.ttf"
    id: bold30
    size: 30
  - file: "fonts/GothamRnd-Bold.ttf"
    id: bold35
    size: 35
  - file: "fonts/GothamRnd-Bold.ttf"
    id: bold40
    size: 40
  - file: "fonts/GothamRnd-Bold.ttf"
    id: bold55
    size: 55
    glyphs: ['.', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '-']
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: mdi120
    size: 120
    glyphs: &mdi-weather-glyphs
      - "\U000F0590" # mdi-weather-cloudy
      - "\U000F0F2F" # mdi-weather-cloudy-alert
      - "\U000F0E6E" # mdi-weather-cloudy-arrow-right
      - "\U000F0591" # mdi-weather-fog
      - "\U000F0592" # mdi-weather-hail
      - "\U000F0F30" # mdi-weather-hazy
      - "\U000F0898" # mdi-weather-hurricane
      - "\U000F0593" # mdi-weather-lightning
      - "\U000F067E" # mdi-weather-lightning-rainy
      - "\U000F0594" # mdi-weather-night
      - "\U000F0F31" # mdi-weather-night-partly-cloudy
      - "\U000F0595" # mdi-weather-partly-cloudy
      - "\U000F0F32" # mdi-weather-partly-lightning
      - "\U000F0F33" # mdi-weather-partly-rainy
      - "\U000F0F34" # mdi-weather-partly-snowy
      - "\U000F0F35" # mdi-weather-partly-snowy-rainy
      - "\U000F0596" # mdi-weather-pouring
      - "\U000F0597" # mdi-weather-rainy
      - "\U000F0598" # mdi-weather-snowy
      - "\U000F0F36" # mdi-weather-snowy-heavy
      - "\U000F067F" # mdi-weather-snowy-rainy
      - "\U000F0599" # mdi-weather-sunny
      - "\U000F0F37" # mdi-weather-sunny-alert
      - "\U000F14E4" # mdi-weather-sunny-off
      - "\U000F059A" # mdi-weather-sunset
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up
      - "\U000F0F38" # mdi-weather-tornado
      - "\U000F059D" # mdi-weather-windy
      - "\U000F059E" # mdi-weather-windy-variant
      - "\U000F00E9" # mdi-cake
      - "\U000F0A70" # mdi-silverware-fork-knife
      - "\U000F1A74" # mdi-solar-power-variant-outline
      - "\U000F1904" # mdi-home-lightning-bolt-outline
      - "\U000F05C3" # mdi-youtube
      - "\U000F05D6" # mdi-alert-circle-outline

  - file: "fonts/materialdesignicons-webfont.ttf"
    id: mdi36
    size: 36

text_sensor:
  - platform: homeassistant
    entity_id: sensor.current_day_name
    id: current_day_name
  - platform: homeassistant
    entity_id: sensor.current_day_number
    id: current_day_number
  - platform: homeassistant
    entity_id: sensor.current_month
    id: current_month

  - platform: homeassistant
    entity_id: sensor.weather_condition
    id: weather_condition
  - platform: homeassistant
    entity_id: sensor.weather_temperature
    id: weather_temperature
  - platform: homeassistant
    entity_id: sensor.weather_icon
    id: weather_icon
  - platform: homeassistant
    entity_id: sensor.weather_icon_0
    id: weather_icon_0
  - platform: homeassistant
    entity_id: sensor.weather_icon1
    id: weather_icon_1
  - platform: homeassistant
    entity_id: sensor.weather_icon_2
    id: weather_icon_2
  - platform: homeassistant
    entity_id: sensor.weather_icon_3
    id: weather_icon_3
  - platform: homeassistant
    entity_id: sensor.weather_icon_4
    id: weather_icon_4
  - platform: homeassistant
    entity_id: sensor.home_condition_day_0d
    id: home_condition_day_0d
  - platform: homeassistant
    entity_id: sensor.home_condition_day_1d
    id: home_condition_day_1d
  - platform: homeassistant
    entity_id: sensor.home_condition_day_2d
    id: home_condition_day_2d
  - platform: homeassistant
    entity_id: sensor.home_condition_day_3d
    id: home_condition_day_3d
  - platform: homeassistant
    entity_id: sensor.home_condition_day_4d
    id: home_condition_day_4d

  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_max_0d
    id: home_realfeel_temperature_max_0d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_max_1d
    id: home_realfeel_temperature_max_1d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_max_2d
    id: home_realfeel_temperature_max_2d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_max_3d
    id: home_realfeel_temperature_max_3d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_max_4d
    id: home_realfeel_temperature_max_4d

  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_min_0d
    id: home_realfeel_temperature_min_0d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_min_1d
    id: home_realfeel_temperature_min_1d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_min_2d
    id: home_realfeel_temperature_min_2d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_min_3d
    id: home_realfeel_temperature_min_3d
  - platform: homeassistant
    entity_id: sensor.home_realfeel_temperature_min_4d
    id: home_realfeel_temperature_min_4d

  - platform: homeassistant
    entity_id: sensor.esp_calendar_data
    attribute: event_title_1
    id: event_title_1
  - platform: homeassistant
    entity_id: sensor.esp_calendar_data
    attribute: event_start_1
    id: event_start_1
  - platform: homeassistant
    entity_id: sensor.esp_calendar_data
    attribute: event_title_2
    id: event_title_2
  - platform: homeassistant
    entity_id: sensor.esp_calendar_data
    attribute: event_start_2
    id: event_start_2

# --- UPDATED DISPLAY PINS ---
display:
  - platform: waveshare_epaper
    reset_duration: 10ms
    id: eink_display
    cs_pin: 5       # was 15
    dc_pin: 17      # was 27
    busy_pin: 4     # was 25
    reset_pin: 16   # was 26
    model: 7.50inv2
    rotation: 270°
    update_interval: never
    lambda: |-
      ESP_LOGI("display", "Updating ePaper Calendar...");
      it.fill(Color(0, 0, 0));

      // Debugging: Log the received date values
      ESP_LOGI("display", "Received Day Name: %s", id(current_day_name).state.c_str());
      ESP_LOGI("display", "Received Day Number: %s", id(current_day_number).state.c_str());
      ESP_LOGI("display", "Received Month: %s", id(current_month).state.c_str());

      // Step 1: Display Today's Date (Centered at the Top)
      if (id(current_day_name).has_state() && 
          id(current_day_number).has_state() && 
          id(current_month).has_state() &&
          !id(current_day_name).state.empty() && 
          !id(current_day_number).state.empty() && 
          !id(current_month).state.empty()) {
          
        std::string formatted_date = id(current_day_name).state + " " + 
                                      id(current_day_number).state + " " + 
                                      id(current_month).state;
        ESP_LOGI("display", "Formatted Date: %s", formatted_date.c_str());
        it.printf(240, 40, id(bold30), TextAlign::CENTER, "%s", formatted_date.c_str());

      } else {
        ESP_LOGE("display", "Date sensors missing!");
        it.printf(240, 40, id(bold30), TextAlign::CENTER, "No Date Available");
      }

      // Step 2: Draw a Horizontal Line
      it.filled_rectangle(50, 90, 380, 2);

      // Step 3: Display Weather Section (Icon + Condition + Temperature)
      std::map<std::string, std::string> weather_icon_map = {
          {"mdi-weather-night", "\U000F0594"}, {"mdi-weather-cloudy", "\U000F0590"},
          {"mdi-weather-fog", "\U000F0591"}, {"mdi-weather-hail", "\U000F0592"},
          {"mdi-weather-lightning", "\U000F0593"}, {"mdi-weather-lightning-rainy", "\U000F067E"},
          {"mdi-weather-partly-cloudy", "\U000F0595"}, {"mdi-weather-pouring", "\U000F0596"},
          {"mdi-weather-rainy", "\U000F0597"}, {"mdi-weather-snowy", "\U000F0598"},
          {"mdi-weather-snowy-rainy", "\U000F067F"}, {"mdi-weather-sunny", "\U000F0599"},
          {"mdi-weather-windy", "\U000F059D"}, {"mdi-help-circle", "\U000F059D"}
      };

      // Get weather icon from the map
      std::string weather_condition_str = id(weather_icon).has_state() ? id(weather_icon).state : "mdi-help-circle";
      std::string weather_icon = weather_icon_map.count(weather_condition_str)
                                 ? weather_icon_map[weather_condition_str]
                                 : "\U000F059D"; // fallback

      ESP_LOGI("display", "Mapped Weather Icon: %s", weather_icon.c_str());

      it.printf(100, 140, id(mdi120), TextAlign::LEFT, "%s", weather_icon.c_str());  // Icon
      it.printf(230, 150, id(bold35), TextAlign::LEFT, "%s", id(weather_condition).state.c_str()); // Condition
      it.printf(230, 190, id(bold55), TextAlign::LEFT, "%s°C", id(weather_temperature).state.c_str()); // Temp

      // Step 4: Draw Another Horizontal Line
      it.filled_rectangle(50, 250, 380, 2);

      int y_pos = 270;

      // Forecast for Day 0
      {
        std::string icon_0 = weather_icon_map[id(weather_icon_0).state];
        // Optional: Show the "Day Name" if you have it. Some folks reuse home_condition_day_0d for something else.
        // it.printf(90, y_pos, id(bold25), TextAlign::LEFT, "%s", id(home_condition_day_0d).state.c_str());
        it.printf(50, y_pos, id(mdi36), TextAlign::LEFT, "%s", icon_0.c_str());
        it.printf(320, y_pos, id(bold20), TextAlign::RIGHT, "%s°C / %s°C",
                  id(home_realfeel_temperature_max_0d).state.c_str(),
                  id(home_realfeel_temperature_min_0d).state.c_str());
        y_pos += 40;
      }

      

If your image is displaying correctly you can ignore these. This warning was introduced about 6 months ago, components took that long to update previously but they didn’t generate the warning message…

The device actually keeps restarting because of that. So I wonder if something is wrong with my config.

Then show what you get on log before it restarts.

As you can see it ends up entering safe mode after multiple reboots.

[10:42:30][C][wifi:428]:   Local MAC: XXXXXXXXXXXXXXX
[10:42:30][C][wifi:433]:   SSID: [redacted]
[10:42:30][C][wifi:436]:   IP Address: 192.168.1.135
[10:42:30][C][wifi:440]:   BSSID: [redacted]
[10:42:30][C][wifi:441]:   Hostname: 'mehdicalendar'
[10:42:30][C][wifi:443]:   Signal strength: -51 dB ▂▄▆█
[10:42:30][C][wifi:447]:   Channel: 1
[10:42:30][C][wifi:448]:   Subnet: xxxxx
[10:42:30][C][wifi:449]:   Gateway: xxxx
[10:42:30][C][wifi:450]:   DNS1: xxxxxxxx
[10:42:30][C][wifi:451]:   DNS2: xxxxx
[10:42:30][D][wifi:626]: Disabling AP...
[10:42:30][I][app:062]: setup() finished successfully!
[10:42:30][W][safe_mode:099]: SAFE MODE IS ACTIVE
[10:42:30][W][component:170]: Component wifi cleared Warning flag
[10:42:30][I][app:100]: ESPHome version 2025.2.0 compiled on Feb 23 2025, 02:12:40
[10:42:30][C][wifi:600]: WiFi:
[10:42:30][C][wifi:428]:   Local MAC: xxxxx
[10:42:30][C][wifi:433]:   SSID: [redacted]
[10:42:30][C][wifi:436]:   IP Address: xxxxxxxx
[10:42:30][C][wifi:440]:   BSSID: [redacted]
[10:42:30][C][wifi:441]:   Hostname: 'mehdicalendar'
[10:42:30][C][wifi:443]:   Signal strength: -52 dB ▂▄▆█
[10:42:30][C][wifi:447]:   Channel: 1
[10:42:30][C][wifi:448]:   Subnet: xxxxxxxx
[10:42:30][C][wifi:449]:   Gateway: xxxxxxxxxx
[10:42:30][C][wifi:450]:   DNS1: xxxxxxxxx
[10:42:30][C][wifi:451]:   DNS2: xxxxxxxxxxx
[10:42:30][C][logger:177]: Logger:
[10:42:30][C][logger:178]:   Max Level: DEBUG
[10:42:30][C][logger:179]:   Initial Level: DEBUG
[10:42:30][C][logger:181]:   Log Baud Rate: 115200
[10:42:30][C][logger:182]:   Hardware UART: UART0
[10:42:30][C][mdns:116]: mDNS:
[10:42:30][C][mdns:117]:   Hostname: mehdicalendar
[10:42:30][C][esphome.ota:073]: Over-The-Air updates:
[10:42:30][C][esphome.ota:074]:   Address: mehdicalendar.local:3232
[10:42:30][C][esphome.ota:075]:   Version: 2
[10:42:30][C][safe_mode:018]: Safe Mode:
[10:42:30][C][safe_mode:020]:   Boot considered successful after 60 seconds
[10:42:30][C][safe_mode:021]:   Invoke after 10 boot attempts
[10:42:30][C][safe_mode:023]:   Remain in safe mode for 300 seconds
[10:42:30][W][safe_mode:031]: SAFE MODE IS ACTIVE

The last lines from Safe Mode: are just informing you how safe mode is configured and that it’s active.
Not that your esp is in safe mode.

After 10 unsuccessful boot attempts you should get message:
“Boot loop detected. Proceeding to safe mode”
before going to safe mode…

This is because I took it out of safe mode so I can continue troubleshooting.

Right now I removed everything from the display, I am displaying only the today’s date and put update interval to 10s (just so I can troubleshoot).
I get this log. As you can see in the log it is displaying the date, but I have always the timeout while displaying image message, and the component display took a long time for an operation.

[11:25:43][D][sntp:067]: Synchronized time: 2025-02-23 11:25:43
[11:25:49][I][waveshare_epaper:3563]: Power on the display and hat
[11:26:00][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:26:11][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:26:11][W][component:237]: Component display took a long time for an operation (21401 ms).
[11:26:11][W][component:238]: Components should block for at most 30 ms.
[11:26:11][I][waveshare_epaper:3563]: Power on the display and hat
[11:26:12][W][component:237]: Component display took a long time for an operation (1383 ms).
[11:26:12][W][component:238]: Components should block for at most 30 ms.
[11:26:19][I][waveshare_epaper:3563]: Power on the display and hat
[11:26:30][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:26:41][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:26:41][W][component:237]: Component display took a long time for an operation (21401 ms).
[11:26:41][W][component:238]: Components should block for at most 30 ms.
[11:26:41][I][waveshare_epaper:3563]: Power on the display and hat
[11:26:42][W][component:237]: Component display took a long time for an operation (1382 ms).
[11:26:42][W][component:238]: Components should block for at most 30 ms.
[11:26:49][I][waveshare_epaper:3563]: Power on the display and hat
[11:27:00][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:27:11][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:27:11][W][component:237]: Component display took a long time for an operation (21401 ms).
[11:27:11][W][component:238]: Components should block for at most 30 ms.
[11:27:11][I][waveshare_epaper:3563]: Power on the display and hat
[11:27:12][W][component:237]: Component display took a long time for an operation (1383 ms).
[11:27:12][W][component:238]: Components should block for at most 30 ms.
[11:27:19][I][waveshare_epaper:3563]: Power on the display and hat
[11:27:30][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:27:41][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:27:41][W][component:237]: Component display took a long time for an operation (21401 ms).
[11:27:41][W][component:238]: Components should block for at most 30 ms.
[11:27:41][I][waveshare_epaper:3563]: Power on the display and hat
[11:27:42][W][component:237]: Component display took a long time for an operation (1382 ms).
[11:27:42][W][component:238]: Components should block for at most 30 ms.
[11:27:49][I][waveshare_epaper:3563]: Power on the display and hat
[11:28:00][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:28:11][E][waveshare_epaper:3508]: Timeout while displaying image!
[11:28:11][W][component:237]: Component display took a long time for an operation (21401 ms).
[11:28:11][W][component:238]: Components should block for at most 30 ms.
[11:28:11][I][waveshare_epaper:3563]: Power on the display and hat
[11:28:12][W][component:237]: Component display took a long time for an operation (1382 ms).
[11:28:12][W][component:238]: Components should block for at most 30 ms.

Look, you are reading your logs incorrectly and posting boot logs that don’t present any problem.

This is only log post showing thin evidence of problem.
But it doesn’t show disconnection or reboot.

Your display component setup is hard to read/debug.
Did you ever try some simple example setup that was working?

How about here, is it where it shows it goes to safe mode?


[12:15:53][D][wifi:482]: Starting scan...
[12:15:53]Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.
[12:15:53]
[12:15:53]Core  1 register dump:
[12:15:53]PC      : 0x4016c5ef  PS      : 0x00060930  A0      : 0x800d91b5  A1      : 0x3ffb1ea0  
[12:15:53]A2      : 0x00000000  A3      : 0x3ffb1fdc  A4      : 0x00000004  A5      : 0x00007ffe  
[12:15:53]A6      : 0x00000001  A7      : 0x3ffb08c0  A8      : 0x00000004  A9      : 0x3ffb1fdf  
[12:15:53]A10     : 0x0000009d  A11     : 0x3f40357b  A12     : 0x3f403592  A13     : 0x3f4014a3  
[12:15:53]A14     : 0x3ffc2f94  A15     : 0x3ffb246c  SAR     : 0x00000010  EXCCAUSE: 0x0000001c  
[12:15:53]EXCVADDR: 0x00000000  LBEG    : 0x40089fa1  LEND    : 0x40089fb1  LCOUNT  : 0xfffffffe  
[12:15:53]
[12:15:53]
[12:15:53]Backtrace:0x4016c5ec:0x3ffb1ea00x400d91b2:0x3ffb1ec0 0x400d9201:0x3ffb1ee0 0x4016c4e1:0x3ffb1f20 0x400d7b3d:0x3ffb1f60 0x400d7ba6:0x3ffb1fb0 0x400d7bef:0x3ffb2100 0x400e469c:0x3ffb2150 0x400e4a61:0x3ffb24a0 0x400d7ee7:0x3ffb24c0 0x400d7f25:0x3ffb24e0 0x400dc249:0x3ffb2500 0x4016cd55:0x3ffb2520 0x400e18f3:0x3ffb2540 0x400e0502:0x3ffb2590 0x400e40ae:0x3ffb25c0 0x400eb872:0x3ffb2820 
[12:15:53]
[12:15:54]
[12:15:54]
[12:15:54]
[12:15:54]ELF file SHA256: 0000000000000000
[12:15:54]
[12:15:54]Rebooting...
[12:15:54]ets Jul 29 2019 12:21:46
[12:15:54]
[12:15:54]rst:0xc (SW_CPU_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
[12:15:54]configsip: 0, SPIWP:0xee
[12:15:54]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:15:54]mode:DIO, clock div:2
[12:15:54]load:0x3fff0030,len:1184
[12:15:54]load:0x40078000,len:13132
[12:15:54]load:0x40080400,len:3036
[12:15:54]entry 0x400805e4
[12:15:54][I][logger:171]: Log initialized
[12:15:54][C][safe_mode:079]: There have been 10 suspected unsuccessful boot attempts
[12:15:54][D][esp32.preferences:114]: Saving 1 preferences to flash...
[12:15:54][D][esp32.preferences:143]: Saving 1 preferences to flash: 0 cached, 1 written, 0 failed
[12:15:54][E][safe_mode:086]: Boot loop detected. Proceeding to safe mode
[12:15:54][E][component:164]: Component safe_mode set Error flag: unspecified
[12:15:54][I][app:029]: Running through setup()...
[12:15:54][C][wifi:048]: Setting up WiFi...
[12:15:54][C][wifi:061]: Starting WiFi...
[12:15:54][C][wifi:062]:   Local MAC: XXXXXXXX

Yes. here:

But how to determine the problem?

This is my lambda block:

lambda: |-
      it.fill(Color(0, 0, 0));

      // ================= TOP: DATE =================
      if (id(current_day_name).has_state() && !id(current_day_name).state.empty() &&
          id(current_day_number).has_state() && !id(current_day_number).state.empty() &&
          id(current_month).has_state()    && !id(current_month).state.empty()) {
        std::string date_str = id(current_day_name).state + " " +
                               id(current_day_number).state + " " +
                               id(current_month).state;
        it.printf(240, 40, id(bold30), TextAlign::CENTER, "%s", date_str.c_str());
      } else {
        it.printf(240, 40, id(bold30), TextAlign::CENTER, "No Date Available");
      }

      // Horizontal line
      it.filled_rectangle(50, 90, 380, 2);

      // ========== CURRENT WEATHER (BIG ICON + TEMP) ==========
      std::map<std::string, std::string> weather_icon_map = {
          {"mdi-weather-night", "\U000F0594"},
          {"mdi-weather-cloudy", "\U000F0590"},
          {"mdi-weather-fog", "\U000F0591"},
          {"mdi-weather-hail", "\U000F0592"},
          {"mdi-weather-lightning", "\U000F0593"},
          {"mdi-weather-lightning-rainy", "\U000F067E"},
          {"mdi-weather-partly-cloudy", "\U000F0595"},
          {"mdi-weather-pouring", "\U000F0596"},
          {"mdi-weather-rainy", "\U000F0597"},
          {"mdi-weather-snowy", "\U000F0598"},
          {"mdi-weather-snowy-rainy", "\U000F067F"},
          {"mdi-weather-sunny", "\U000F0599"},
          {"mdi-weather-windy", "\U000F059D"},
          {"mdi-help-circle", "\U000F059D"}  // fallback
      };

      // Today icon
      std::string today_icon_str = id(weather_icon).has_state()
        ? id(weather_icon).state
        : "mdi-help-circle";
      std::string today_icon_glyph = weather_icon_map.count(today_icon_str)
        ? weather_icon_map[today_icon_str]
        : "\U000F059D";

      it.printf(100, 140, id(mdi120), TextAlign::LEFT, "%s", today_icon_glyph.c_str());
      it.printf(230, 190, id(bold55), TextAlign::LEFT, "%s°C", id(weather_temperature).state.c_str());

      // Another horizontal line
      it.filled_rectangle(50, 250, 380, 2);

      // ========== 5-DAY FORECAST (ICONS + MIN/MAX ONLY) ==========
      int x_start = 60;         // left offset for first day
      const int col_w = 80;     // column width
      int y_icon = 300;         // row for icon
      int y_temp = 340;         // row for min/max temps

      // ----- Day 0 -----
      {
        std::string icon0_str = id(weather_icon_0).has_state()
                                ? id(weather_icon_0).state
                                : "mdi-help-circle";
        std::string icon0_glyph = weather_icon_map.count(icon0_str)
                                  ? weather_icon_map[icon0_str]
                                  : "\U000F059D";
        it.printf(x_start, y_icon, id(mdi36), TextAlign::CENTER, "%s", icon0_glyph.c_str());
        it.printf(
          x_start, y_temp, id(bold20), TextAlign::CENTER, "%s°C / %s°C",
          id(home_realfeel_temperature_max_0d).state.c_str(),
          id(home_realfeel_temperature_min_0d).state.c_str()
        );
      }
      x_start += col_w;

      // ----- Day 1 -----
      {
        std::string icon1_str = id(weather_icon_1).has_state()
                                ? id(weather_icon_1).state
                                : "mdi-help-circle";
        std::string icon1_glyph = weather_icon_map.count(icon1_str)
                                  ? weather_icon_map[icon1_str]
                                  : "\U000F059D";
        it.printf(x_start, y_icon, id(mdi36), TextAlign::CENTER, "%s", icon1_glyph.c_str());
        it.printf(
          x_start, y_temp, id(bold20), TextAlign::CENTER, "%s°C / %s°C",
          id(home_realfeel_temperature_max_1d).state.c_str(),
          id(home_realfeel_temperature_min_1d).state.c_str()
        );
      }
      x_start += col_w;

      // ----- Day 2 -----
      {
        std::string icon2_str = id(weather_icon_2).has_state()
                                ? id(weather_icon_2).state
                                : "mdi-help-circle";
        std::string icon2_glyph = weather_icon_map.count(icon2_str)
                                  ? weather_icon_map[icon2_str]
                                  : "\U000F059D";
        it.printf(x_start, y_icon, id(mdi36), TextAlign::CENTER, "%s", icon2_glyph.c_str());
        it.printf(
          x_start, y_temp, id(bold20), TextAlign::CENTER, "%s°C / %s°C",
          id(home_realfeel_temperature_max_2d).state.c_str(),
          id(home_realfeel_temperature_min_2d).state.c_str()
        );
      }
      x_start += col_w;

      // ----- Day 3 -----
      {
        std::string icon3_str = id(weather_icon_3).has_state()
                                ? id(weather_icon_3).state
                                : "mdi-help-circle";
        std::string icon3_glyph = weather_icon_map.count(icon3_str)
                                  ? weather_icon_map[icon3_str]
                                  : "\U000F059D";
        it.printf(x_start, y_icon, id(mdi36), TextAlign::CENTER, "%s", icon3_glyph.c_str());
        it.printf(
          x_start, y_temp, id(bold20), TextAlign::CENTER, "%s°C / %s°C",
          id(home_realfeel_temperature_max_3d).state.c_str(),
          id(home_realfeel_temperature_min_3d).state.c_str()
        );
      }
      x_start += col_w;

      // ----- Day 4 -----
      {
        std::string icon4_str = id(weather_icon_4).has_state()
                                ? id(weather_icon_4).state
                                : "mdi-help-circle";
        std::string icon4_glyph = weather_icon_map.count(icon4_str)
                                  ? weather_icon_map[icon4_str]
                                  : "\U000F059D";
        it.printf(x_start, y_icon, id(mdi36), TextAlign::CENTER, "%s", icon4_glyph.c_str());
        it.printf(
          x_start, y_temp, id(bold20), TextAlign::CENTER, "%s°C / %s°C",
          id(home_realfeel_temperature_max_4d).state.c_str(),
          id(home_realfeel_temperature_min_4d).state.c_str()
        );
      }

When I display only the date up until where it says // ========== 5-DAY FORECAST (ICONS + MIN/MAX ONLY) ========== , it runs fine (but still get the image timeout message.

When I add the lines below the // ========== 5-DAY FORECAST (ICONS + MIN/MAX ONLY) ========== , that is where it enters the safe mode.

Also this?
[11:28:11][W][component:237]: Component display took a long time for an operation (21401 ms).

It reboots 10 times before going to safe mode. Try to catch the log just before it reboots to see what is really causing it…

I am pretty sure that over 20s to update the display is the problem. There is a watch dog that has to be petted regularly or it gets angry and reboots the system.

That display update takes orders of magnitude longer than what is generally considered good practice and likely just too long to ever work. The likely better way to do is something that breaks up the big update into multiple smaller updates that each take much less time and give the rest of the system time to take care of things.