sleep: allow HibernateDelaySec and low-battery hibernation to work together

Previously, setting HibernateDelaySec= would disable ACPI battery trip
point (_BTP) alarms, forcing the system to rely solely on software
polling for battery checks. This could result in the battery draining
to 0% between polling intervals, causing data loss.

Now, when ACPI _BTP is available AND HibernateDelaySec= is set, both
mechanisms work together. The system will hibernate on whichever comes
first: low battery (instant hardware alarm) or the configured timeout.

This also properly respects HibernateOnACPower=no by resetting the
timer while on AC power, matching the documented behavior.

Fixes: https://github.com/systemd/systemd/issues/26498
This commit is contained in:
gvenugo3
2026-02-05 10:28:47 +01:00
committed by Lennart Poettering
parent 2ac64e5715
commit 8950f64263
2 changed files with 81 additions and 27 deletions
+7 -6
View File
@@ -96,12 +96,13 @@
<para>If the system has no battery, it would be hibernated after <varname>HibernateDelaySec=</varname>
has passed. If not set, then defaults to <literal>2h</literal>.</para>
<para>If the system has battery and <varname>HibernateDelaySec=</varname> is not set, low-battery
alarms (ACPI _BTP) are tried first for detecting battery percentage and wake up the system for hibernation.
If not available, or <varname>HibernateDelaySec=</varname> is set, the system would regularly wake
up to check the time and detect the battery percentage/discharging rate. The rate is used to
schedule the next detection. If that is also not available, <varname>SuspendEstimationSec=</varname>
is used as last resort.</para>
<para>If the system has battery, low-battery alarms (ACPI _BTP) are tried first for detecting
battery percentage and wake up the system for hibernation. If <varname>HibernateDelaySec=</varname>
is also set, an additional timer is configured so that the system hibernates on whichever comes
first: low battery or the configured delay. If ACPI _BTP is not available, the system would
regularly wake up to check the time and detect the battery percentage/discharging rate. The rate
is used to schedule the next detection. If that is also not available,
<varname>SuspendEstimationSec=</varname> is used as last resort.</para>
<xi:include href="version-info.xml" xpointer="v239"/>
</listitem>
+61 -8
View File
@@ -507,13 +507,14 @@ static int custom_timer_suspend(const SleepConfig *sleep_config) {
}
static int execute_s2h(const SleepConfig *sleep_config) {
_cleanup_close_ int tfd = -EBADF;
usec_t hibernate_timestamp = 0;
int r;
assert(sleep_config);
/* Only check if we have automated battery alarms if HibernateDelaySec= is not set, as in that case
* we'll busy poll for the configured interval instead */
if (!timestamp_is_set(sleep_config->hibernate_delay_usec)) {
/* Always check if we have automated battery alarms, regardless of HibernateDelaySec= setting.
* This allows both low-battery hibernation AND timeout-based hibernation to work together. */
r = check_wakeup_type();
if (r < 0)
log_warning_errno(r, "Failed to check hardware wakeup type, ignoring: %m");
@@ -522,10 +523,41 @@ static int execute_s2h(const SleepConfig *sleep_config) {
if (r < 0)
log_warning_errno(r, "Failed to check whether acpi_btp support is enabled or not, ignoring: %m");
}
} else
r = 0; /* Force fallback path */
if (r > 0) { /* If we have both wakeup alarms and battery trip point support, use them */
if (r > 0) {
/* We have hardware battery alarm support (ACPI _BTP). If HibernateDelaySec= is also set,
* set up an RTC alarm so we hibernate on whichever comes first: low battery or timeout. */
if (timestamp_is_set(sleep_config->hibernate_delay_usec)) {
tfd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK|TFD_CLOEXEC);
if (tfd < 0)
return log_error_errno(errno, "Error creating timerfd: %m");
hibernate_timestamp = usec_add(now(CLOCK_BOOTTIME), sleep_config->hibernate_delay_usec);
}
for (;;) {
if (tfd >= 0) {
struct itimerspec ts = {};
usec_t time_left;
/* Handle HibernateOnACPower=no: reset timer while on AC power */
if (!sleep_config->hibernate_on_ac_power && on_ac_power() > 0) {
log_debug("On AC power with HibernateOnACPower=no, resetting hibernate timer");
hibernate_timestamp = usec_add(now(CLOCK_BOOTTIME), sleep_config->hibernate_delay_usec);
}
time_left = usec_sub_unsigned(hibernate_timestamp, now(CLOCK_BOOTTIME));
if (time_left <= 0)
break; /* Timer expired, hibernate */
log_debug("Set timerfd wake alarm for %s",
FORMAT_TIMESPAN(time_left, USEC_PER_SEC));
timespec_store(&ts.it_value, time_left);
if (timerfd_settime(tfd, 0, &ts, NULL) < 0)
return log_error_errno(errno, "Error setting hibernate delay timer: %m");
}
log_debug("Attempting to suspend...");
r = execute(sleep_config, SLEEP_SUSPEND, NULL);
if (r < 0)
@@ -534,10 +566,31 @@ static int execute_s2h(const SleepConfig *sleep_config) {
r = check_wakeup_type();
if (r < 0)
return log_error_errno(r, "Failed to check hardware wakeup type: %m");
if (r > 0) {
/* APM Timer wakeup - this means the battery alarm triggered, hibernate */
log_debug("Woken by APM Timer (battery low), proceeding to hibernate");
break;
}
if (r == 0)
/* For APM Timer wakeup, system should hibernate else wakeup */
if (tfd >= 0) {
/* Check if our HibernateDelaySec timer fired */
r = fd_wait_for_event(tfd, POLLIN, 0);
if (r < 0)
return log_error_errno(r, "Error polling timerfd: %m");
if (FLAGS_SET(r, POLLIN)) {
/* Timer fired - but respect HibernateOnACPower setting */
if (!sleep_config->hibernate_on_ac_power && on_ac_power() > 0) {
log_debug("Timer fired but on AC power with HibernateOnACPower=no, continuing suspend");
continue;
}
log_debug("HibernateDelaySec timeout reached, proceeding to hibernate");
break;
}
}
/* Manual wakeup - not battery alarm and not timer */
return 0;
}
} else {
r = custom_timer_suspend(sleep_config);
if (r < 0)