Describe the bug
Every OTA update silently rolls back to the previous firmware on the first battery-powered wake. The device appears to flash successfully, but after the next PMIC-shutdown sleep cycle it boots the old firmware again with no error in logs.
Root cause
ESPHome enables CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE by default for all ESP32 builds (CONF_ENABLE_OTA_ROLLBACK: true in the esp32: framework component). After an OTA flash, the new firmware partition is in ESP_OTA_IMG_PENDING_VERIFY state and must call esp_ota_mark_app_valid_cancel_rollback() before the device reboots, otherwise the bootloader rolls back to the previous partition on the next cold boot.
On a normal battery wake, PMIC full-shutdown (SYS_CMD_OFF) fires at approximately 35–45 seconds into boot — before esp_ota_mark_app_valid_cancel_rollback() is ever called. Since the device is completely powerless during PMIC shutdown, every wake is a full cold boot, and the bootloader sees the firmware as still-pending and rolls back.
This is not visible in logs — the rollback happens silently in the bootloader before ESPHome starts. The only way to detect it is to check a Build DateTime template sensor across wakes.
Why it isn't obvious
The rollback is silent and the symptoms look identical to a failed OTA flash. It only affects battery wakes — USB-connected OTAs are unaffected because the device stays awake long enough for the bootloader's implicit validity check. The problem is also specific to PMIC full-shutdown: ESP32 deep sleep does not produce a cold boot, so the bootloader OTA check never re-triggers on a deep sleep wake.
Fix
Call esp_ota_mark_app_valid_cancel_rollback() before prepare_for_pmic_shutdown() in both the normal (on_download_finished) path and the 90s safety net. In your YAML this means adding a lambda immediately before each shutdown call:
on_download_finished:
- delay: 150ms
- component.update: epd
- delay: 16s
- light.turn_off: status_led
- if:
condition:
lambda: 'return !id(epd_power)->is_usb_present();'
then:
- lambda: |-
esp_ota_mark_app_valid_cancel_rollback();
ESP_LOGI("OTA", "Boot marked valid — OTA rollback cancelled");
- logger.log: "On battery -> PMIC full shutdown, self-wake in 15 min"
- lambda: 'id(epd_power)->prepare_for_pmic_shutdown(900);'
And in the 90s safety net:
- lambda: |-
esp_ota_mark_app_valid_cancel_rollback();
ESP_LOGI("OTA", "Boot marked valid — OTA rollback cancelled");
- lambda: 'id(epd_power)->prepare_for_pmic_shutdown(900);'
No additional headers are required — the function is transitively available via the OTA component.
Describe the bug
Every OTA update silently rolls back to the previous firmware on the first battery-powered wake. The device appears to flash successfully, but after the next PMIC-shutdown sleep cycle it boots the old firmware again with no error in logs.
Root cause
ESPHome enables CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE by default for all ESP32 builds (CONF_ENABLE_OTA_ROLLBACK: true in the esp32: framework component). After an OTA flash, the new firmware partition is in ESP_OTA_IMG_PENDING_VERIFY state and must call esp_ota_mark_app_valid_cancel_rollback() before the device reboots, otherwise the bootloader rolls back to the previous partition on the next cold boot.
On a normal battery wake, PMIC full-shutdown (SYS_CMD_OFF) fires at approximately 35–45 seconds into boot — before esp_ota_mark_app_valid_cancel_rollback() is ever called. Since the device is completely powerless during PMIC shutdown, every wake is a full cold boot, and the bootloader sees the firmware as still-pending and rolls back.
This is not visible in logs — the rollback happens silently in the bootloader before ESPHome starts. The only way to detect it is to check a Build DateTime template sensor across wakes.
Why it isn't obvious
The rollback is silent and the symptoms look identical to a failed OTA flash. It only affects battery wakes — USB-connected OTAs are unaffected because the device stays awake long enough for the bootloader's implicit validity check. The problem is also specific to PMIC full-shutdown: ESP32 deep sleep does not produce a cold boot, so the bootloader OTA check never re-triggers on a deep sleep wake.
Fix
Call esp_ota_mark_app_valid_cancel_rollback() before prepare_for_pmic_shutdown() in both the normal (on_download_finished) path and the 90s safety net. In your YAML this means adding a lambda immediately before each shutdown call:
And in the 90s safety net:
No additional headers are required — the function is transitively available via the OTA component.