1
0
mirror of https://github.com/systemd/systemd.git synced 2024-10-27 10:25:37 +03:00

Merge pull request #23640 from cerebro1/week1

Estimate battery level
This commit is contained in:
Luca Boccassi 2022-07-20 18:07:22 +01:00 committed by GitHub
commit 74c1cf6267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 267 additions and 29 deletions

View File

@ -78,9 +78,20 @@
<term>suspend-then-hibernate</term>
<listitem><para>A low power state where the system is initially suspended
(the state is stored in RAM). If not interrupted within the delay specified by
<command>HibernateDelaySec=</command>, the system will be woken using an RTC
alarm and hibernated (the state is then stored on disk).
(the state is stored in RAM) and then hibernated based on battery percentage
capacity. If the current battery capacity is higher than 5%, the system goes
back to suspend for interval calculated using battery disharge rate per hour.
Battery discharge rate per hour is stored in a file which is created after
initial suspend-resume cycle. The value is calculated using battery decreasing
charge level a timespan. In case of manual wakeup before RTC alarm, the timespan
is the duration for which system was suspended. If the file does not exist or
has invalid value, initial suspend duration is set to
<command>HibernateDelaySec=</command>.
After wakeup via an RTC alarm, the battery discharge rate per hour is again
estimated. If the current battery charge level is equal to or less than 5%,
the system will be hibernated (the state is then stored on disk) else the
system goes back to suspend for the amount of time it would take to fully
discharge the battery minus 30 minutes.
</para></listitem>
</varlistentry>
@ -173,9 +184,9 @@
</varlistentry>
<varlistentry>
<term><varname>HibernateDelaySec=</varname></term>
<listitem><para>The amount of time the system spends in suspend mode before the system is
automatically put into hibernate mode, when using
<listitem><para>The amount of time the system spends in suspend mode
before the RTC alarm wakes the system, before the battery discharge rate
can be estimated and used instead to calculate the suspension interval.
<citerefentry><refentrytitle>systemd-suspend-then-hibernate.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>. Defaults
to 2h.</para></listitem>
</varlistentry>

View File

@ -25,10 +25,13 @@
#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "hexdecoct.h"
#include "id128-util.h"
#include "log.h"
#include "macro.h"
#include "path-util.h"
#include "sleep-config.h"
#include "siphash24.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "string-table.h"
@ -36,6 +39,9 @@
#include "strv.h"
#include "time-util.h"
#define DISCHARGE_RATE_FILEPATH "/var/lib/systemd/sleep/battery_discharge_percentage_rate_per_hour"
#define BATTERY_DISCHARGE_RATE_HASH_KEY SD_ID128_MAKE(5f,9a,20,18,38,76,46,07,8d,36,58,0b,bb,c4,e0,63)
int parse_sleep_config(SleepConfig **ret_sleep_config) {
_cleanup_(free_sleep_configp) SleepConfig *sc = NULL;
int allow_suspend = -1, allow_hibernate = -1,
@ -102,6 +108,169 @@ int parse_sleep_config(SleepConfig **ret_sleep_config) {
return 0;
}
/* If battery percentage capacity is less than equal to 5% return success */
int battery_is_low(void) {
int r;
/* We have not used battery capacity_level since value is set to full
* or Normal in case acpi is not working properly. In case of no battery
* 0 will be returned and system will be suspended for 1st cycle then hibernated */
r = read_battery_capacity_percentage();
if (r == -ENOENT)
return false;
if (r < 0)
return r;
return r <= 5;
}
/* Battery percentage capacity fetched from capacity file and if in range 0-100 then returned */
int read_battery_capacity_percentage(void) {
_cleanup_free_ char *bat_cap = NULL;
int battery_capacity, r;
r = read_one_line_file("/sys/class/power_supply/BAT0/capacity", &bat_cap);
if (r == -ENOENT)
return log_debug_errno(r, "/sys/class/power_supply/BAT0/capacity is unavailable. Assuming no battery exists: %m");
if (r < 0)
return log_debug_errno(r, "Failed to read /sys/class/power_supply/BAT0/capacity: %m");
r = safe_atoi(bat_cap, &battery_capacity);
if (r < 0)
return log_debug_errno(r, "Failed to parse battery capacity: %m");
if (battery_capacity < 0 || battery_capacity > 100)
return log_debug_errno(SYNTHETIC_ERRNO(ERANGE), "Invalid battery capacity");
return battery_capacity;
}
/* Read file path and return hash of value in that file */
static int get_battery_identifier(const char *filepath, struct siphash *ret) {
_cleanup_free_ char *value = NULL;
int r;
assert(filepath);
assert(ret);
r = read_one_line_file(filepath, &value);
if (r == -ENOENT)
log_debug_errno(r, "%s is unavailable: %m", filepath);
else if (r < 0)
return log_debug_errno(r, "Failed to read %s: %m", filepath);
else if (isempty(value))
log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "%s is empty: %m", filepath);
else
siphash24_compress_string(value, ret);
return 0;
}
/* Read system and battery identifier from specific location and generate hash of it */
static int get_system_battery_identifier_hash(uint64_t *ret_hash) {
struct siphash state;
sd_id128_t machine_id, product_id;
int r;
assert(ret_hash);
siphash24_init(&state, BATTERY_DISCHARGE_RATE_HASH_KEY.bytes);
get_battery_identifier("/sys/class/power_supply/BAT0/manufacturer", &state);
get_battery_identifier("/sys/class/power_supply/BAT0/model_name", &state);
get_battery_identifier("/sys/class/power_supply/BAT0/serial_number", &state);
r = sd_id128_get_machine(&machine_id);
if (r == -ENOENT)
log_debug_errno(r, "machine ID is unavailable: %m");
else if (r < 0)
return log_debug_errno(r, "Failed to get machine ID: %m");
else
siphash24_compress(&machine_id, sizeof(sd_id128_t), &state);
r = id128_get_product(&product_id);
if (r == -ENOENT)
log_debug_errno(r, "product_id does not exist: %m");
else if (r < 0)
return log_debug_errno(r, "Failed to get product ID: %m");
else
siphash24_compress(&product_id, sizeof(sd_id128_t), &state);
*ret_hash = siphash24_finalize(&state);
return 0;
}
/* battery percentage discharge rate per hour is in range 1-199 then return success */
static bool battery_discharge_rate_is_valid(int battery_discharge_rate) {
return battery_discharge_rate > 0 && battery_discharge_rate < 200;
}
/* Battery percentage discharge rate per hour is read from specific file. It is stored along with system
* and battery identifier hash to maintain the integrity of discharge rate value */
int get_battery_discharge_rate(void) {
_cleanup_free_ char *hash_id_discharge_rate = NULL, *stored_hash_id = NULL, *stored_discharge_rate = NULL;
const char *p;
uint64_t current_hash_id, hash_id;
int discharge_rate, r;
r = read_one_line_file(DISCHARGE_RATE_FILEPATH, &hash_id_discharge_rate);
if (r < 0)
return log_debug_errno(r, "Failed to read discharge rate from %s: %m", DISCHARGE_RATE_FILEPATH);
p = hash_id_discharge_rate;
r = extract_many_words(&p, " ", 0, &stored_hash_id, &stored_discharge_rate, NULL);
if (r < 0)
return log_debug_errno(r, "Failed to parse hash_id and discharge_rate read from %s location: %m", DISCHARGE_RATE_FILEPATH);
if (r != 2)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid number of items fetched from %s", DISCHARGE_RATE_FILEPATH);
r = safe_atou64(stored_hash_id, &hash_id);
if (r < 0)
return log_debug_errno(r, "Failed to parse discharge rate read from %s location: %m", DISCHARGE_RATE_FILEPATH);
r = get_system_battery_identifier_hash(&current_hash_id);
if (r < 0)
return log_debug_errno(r, "Failed to generate system battery identifier hash: %m");
if(current_hash_id != hash_id)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Current identifier does not match stored identifier: %m");
r = safe_atoi(stored_discharge_rate, &discharge_rate);
if (r < 0)
return log_debug_errno(r, "Failed to parse discharge rate read from %s location: %m", DISCHARGE_RATE_FILEPATH);
if (!battery_discharge_rate_is_valid(discharge_rate))
return log_debug_errno(SYNTHETIC_ERRNO(ERANGE), "Invalid battery discharge percentage rate per hour: %m");
return discharge_rate;
}
/* Write battery percentage discharge rate per hour along with system and battery identifier hash to file */
int put_battery_discharge_rate(int estimated_battery_discharge_rate) {
uint64_t system_hash_id;
int r;
if (!battery_discharge_rate_is_valid(estimated_battery_discharge_rate))
return log_debug_errno(SYNTHETIC_ERRNO(ERANGE), "Invalid battery discharge percentage rate per hour: %m");
r = get_system_battery_identifier_hash(&system_hash_id);
if (r < 0)
return log_debug_errno(r, "Failed to generate system battery identifier hash: %m");
r = write_string_filef(
DISCHARGE_RATE_FILEPATH,
WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755,
"%"PRIu64" %d",
system_hash_id,
estimated_battery_discharge_rate);
if (r < 0)
return log_debug_errno(r, "Failed to create %s: %m", DISCHARGE_RATE_FILEPATH);
log_debug("Estimated discharge rate %d successfully updated to %s", estimated_battery_discharge_rate, DISCHARGE_RATE_FILEPATH);
return 0;
}
int can_sleep_state(char **types) {
_cleanup_free_ char *text = NULL;
int r;

View File

@ -55,6 +55,10 @@ int find_hibernate_location(HibernateLocation **ret_hibernate_location);
int can_sleep(SleepOperation operation);
int can_sleep_disk(char **types);
int can_sleep_state(char **types);
int read_battery_capacity_percentage(void);
int battery_is_low(void);
int get_battery_discharge_rate(void);
int put_battery_discharge_rate(int estimated_battery_discharge_rate);
const char* sleep_operation_to_string(SleepOperation s) _const_;
SleepOperation sleep_operation_from_string(const char *s) _pure_;

View File

@ -263,41 +263,95 @@ static int execute(
}
static int execute_s2h(const SleepConfig *sleep_config) {
_cleanup_close_ int tfd = -1;
struct itimerspec ts = {};
int r;
assert(sleep_config);
tfd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK|TFD_CLOEXEC);
if (tfd < 0)
return log_error_errno(errno, "Error creating timerfd: %m");
while (battery_is_low() == 0) {
_cleanup_close_ int tfd = -1;
struct itimerspec ts = {};
usec_t suspend_interval = sleep_config->hibernate_delay_sec, before_timestamp = 0, after_timestamp = 0;
bool woken_by_timer;
int last_capacity = 0, current_capacity = 0, previous_discharge_rate, estimated_discharge_rate = 0;
log_debug("Set timerfd wake alarm for %s",
FORMAT_TIMESPAN(sleep_config->hibernate_delay_sec, USEC_PER_SEC));
tfd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK|TFD_CLOEXEC);
if (tfd < 0)
return log_error_errno(errno, "Error creating timerfd: %m");
timespec_store(&ts.it_value, sleep_config->hibernate_delay_sec);
/* Store current battery capacity and current time before suspension */
r = read_battery_capacity_percentage();
if (r >= 0) {
last_capacity = r;
log_debug("Current battery charge percentage: %d%%", last_capacity);
before_timestamp = now(CLOCK_BOOTTIME);
} else if (r == -ENOENT)
/* In case of no battery, system suspend interval will be set to HibernateDelaySec=. */
log_debug_errno(r, "Suspend Interval value set to %s: %m", FORMAT_TIMESPAN(suspend_interval, USEC_PER_SEC));
else
return log_error_errno(r, "Error fetching battery capacity percentage: %m");
r = timerfd_settime(tfd, 0, &ts, NULL);
if (r < 0)
return log_error_errno(errno, "Error setting hibernate timer: %m");
r = get_battery_discharge_rate();
if (r < 0)
log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, "Failed to get discharge rate, ignoring: %m");
else if (last_capacity * 2 <= r) {
/* System should hibernate in case discharge rate is higher than double of battery current capacity
* why double : Because while calculating suspend interval, we have taken a buffer of 30 minute and
* discharge_rate is calculated on per 60 minute basis which is double. Also suspend_interval > 0 */
log_debug("Current battery percentage capacity too low to suspend, so invoking hibernation");
break;
} else {
previous_discharge_rate = r;
assert(previous_discharge_rate != 0);
suspend_interval = usec_sub_unsigned(last_capacity * USEC_PER_HOUR / previous_discharge_rate, 30 * USEC_PER_MINUTE);
/* The previous discharge rate is stored in per hour basis so converted to minutes.
* Substracted 30 minutes from the result to keep a buffer of 30 minutes before battery gets critical */
}
log_debug("Set timerfd wake alarm for %s", FORMAT_TIMESPAN(suspend_interval, USEC_PER_SEC));
/* Wake alarm for system with or without battery to hibernate or estimate discharge rate whichever is applicable */
timespec_store(&ts.it_value, suspend_interval);
r = execute(sleep_config, SLEEP_SUSPEND, NULL);
if (r < 0)
return r;
if (timerfd_settime(tfd, 0, &ts, NULL) < 0)
return log_error_errno(errno, "Error setting battery estimate timer: %m");
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)) /* We woke up before the alarm time, we are done. */
return 0;
r = execute(sleep_config, SLEEP_SUSPEND, NULL);
if (r < 0)
return r;
tfd = safe_close(tfd);
r = fd_wait_for_event(tfd, POLLIN, 0);
if (r < 0)
return log_error_errno(r, "Error polling timerfd: %m");
/* Store fd_wait status */
woken_by_timer = FLAGS_SET(r, POLLIN);
/* If woken up after alarm time, hibernate */
log_debug("Attempting to hibernate after waking from %s timer",
FORMAT_TIMESPAN(sleep_config->hibernate_delay_sec, USEC_PER_SEC));
r = read_battery_capacity_percentage();
if (r >= 0) {
current_capacity = r;
log_debug("Current battery charge percentage after wakeup: %d%%", current_capacity);
} else if (r == -ENOENT) {
/* In case of no battery, system will be hibernated after 1st cycle of suspend */
log_debug_errno(r, "Battery capacity percentage unavailable, cannot estimate discharge rate: %m");
break;
} else
return log_error_errno(r, "Error fetching battery capacity percentage: %m");
if (current_capacity >= last_capacity)
log_debug("Battery was not discharged during suspension");
else {
after_timestamp = now(CLOCK_BOOTTIME);
log_debug("Attempting to estimate battery discharge rate after wakeup from %s sleep", FORMAT_TIMESPAN(after_timestamp - before_timestamp, USEC_PER_HOUR));
estimated_discharge_rate = (last_capacity - current_capacity) * USEC_PER_HOUR / (after_timestamp - before_timestamp);
r = put_battery_discharge_rate(estimated_discharge_rate);
if (r < 0)
log_warning_errno(r, "Failed to update battery discharge rate, ignoring: %m");
}
if (!woken_by_timer)
/* Return as manual wakeup done. This also will return in case battery was charged during suspension */
return 0;
}
log_debug("Attempting to hibernate");
r = execute(sleep_config, SLEEP_HIBERNATE, NULL);
if (r < 0) {
log_notice("Couldn't hibernate, will try to suspend again.");