From dc072f9f5353dc8adab14b2de99f0c1abb25e8d8 Mon Sep 17 00:00:00 2001 From: Tommie Gannert Date: Mon, 1 Aug 2016 18:03:03 +0100 Subject: [PATCH] Implement aggregates for batteries. Using title number all, this enables aggregates. Note that FreeBSD and OpenBSD previously only reported aggregates, so this is bringing Linux and NetBSD that functionality. Changes the default battery reporting to the aggregate since most users probably don't care about individual batteries. For single-battery systems there should be no change. Fixes one obvious memory leak in NetBSD. --- i3status.c | 2 +- i3status.conf | 4 +- include/i3status.h | 5 - man/i3status.man | 12 ++- src/print_battery_info.c | 217 +++++++++++++++++++++++++++++++++------ 5 files changed, 198 insertions(+), 42 deletions(-) diff --git a/i3status.c b/i3status.c index 892d4aa..5e17745 100644 --- a/i3status.c +++ b/i3status.c @@ -664,7 +664,7 @@ int main(int argc, char *argv[]) { CASE_SEC_TITLE("battery") { SEC_OPEN_MAP("battery"); - print_battery_info(json_gen, buffer, atoi(title), cfg_getstr(sec, "path"), cfg_getstr(sec, "format"), cfg_getstr(sec, "format_down"), cfg_getstr(sec, "status_chr"), cfg_getstr(sec, "status_bat"), cfg_getstr(sec, "status_unk"), cfg_getstr(sec, "status_full"), cfg_getint(sec, "low_threshold"), cfg_getstr(sec, "threshold_type"), cfg_getbool(sec, "last_full_capacity"), cfg_getbool(sec, "integer_battery_capacity"), cfg_getbool(sec, "hide_seconds")); + print_battery_info(json_gen, buffer, (strcasecmp(title, "all") == 0 ? -1 : atoi(title)), cfg_getstr(sec, "path"), cfg_getstr(sec, "format"), cfg_getstr(sec, "format_down"), cfg_getstr(sec, "status_chr"), cfg_getstr(sec, "status_bat"), cfg_getstr(sec, "status_unk"), cfg_getstr(sec, "status_full"), cfg_getint(sec, "low_threshold"), cfg_getstr(sec, "threshold_type"), cfg_getbool(sec, "last_full_capacity"), cfg_getbool(sec, "integer_battery_capacity"), cfg_getbool(sec, "hide_seconds")); SEC_CLOSE_MAP; } diff --git a/i3status.conf b/i3status.conf index 2d71a54..7f37964 100644 --- a/i3status.conf +++ b/i3status.conf @@ -15,7 +15,7 @@ order += "ipv6" order += "disk /" order += "wireless _first_" order += "ethernet _first_" -order += "battery 0" +order += "battery all" order += "load" order += "tztime local" @@ -30,7 +30,7 @@ ethernet _first_ { format_down = "E: down" } -battery 0 { +battery all { format = "%status %percentage %remaining" } diff --git a/include/i3status.h b/include/i3status.h index 4d2d0f1..3911e04 100644 --- a/include/i3status.h +++ b/include/i3status.h @@ -168,11 +168,6 @@ char *pct_mark; } \ } while (0) -typedef enum { CS_DISCHARGING, - CS_CHARGING, - CS_UNKNOWN, - CS_FULL } charging_status_t; - /* * The "min_width" module option may either be defined as a string or a number. */ diff --git a/man/i3status.man b/man/i3status.man index d0ca2a0..cdde2aa 100644 --- a/man/i3status.man +++ b/man/i3status.man @@ -336,6 +336,10 @@ colored red. The low_threshold type can be of threshold_type "time" or "percentage". So, if you configure low_threshold to 10 and threshold_type to "time", and your battery lasts another 9 minutes, it will be colored red. +To show an aggregate of all batteries in the system, use "all" as the number. In +this case (for Linux), the /sys path must contain the "%d" sequence. Otherwise, +the number indicates the battery index as reported in /sys. + Optionally custom strings including any UTF-8 symbols can be used for different battery states. This makes it possible to display individual symbols for each state (charging, discharging, unknown, full) @@ -343,7 +347,9 @@ Of course it will also work with special iconic fonts, such as FontAwesome. If any of these special status strings are omitted, the default (CHR, BAT, UNK, FULL) is used. -*Example order*: +battery 0+ +*Example order (for the first battery)*: +battery 0+ + +*Example order (aggregate of all batteries)*: +battery all+ *Example format*: +%status %remaining (%emptytime %consumption)+ @@ -361,7 +367,9 @@ FULL) is used. *Example threshold_type*: +time+ -*Example path*: +/sys/class/power_supply/CMB1/uevent+ +*Example path (%d replaced by title number)*: +/sys/class/power_supply/CMB%d/uevent+ + +*Example path (ignoring the number)*: +/sys/class/power_supply/CMB1/uevent+ === CPU-Temperature diff --git a/src/print_battery_info.c b/src/print_battery_info.c index 27473a2..52d5031 100644 --- a/src/print_battery_info.c +++ b/src/print_battery_info.c @@ -9,6 +9,12 @@ #include "i3status.h" +#if defined(LINUX) +#include +#include +#include +#endif + #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__DragonFly__) #include #include @@ -28,17 +34,81 @@ #include #endif -struct battery_info { - int full_design; - int full_last; - int remaining; +typedef enum { + CS_UNKNOWN, + CS_DISCHARGING, + CS_CHARGING, + CS_FULL, +} charging_status_t; - int present_rate; +/* A description of the state of one or more batteries. */ +struct battery_info { + /* measured properties */ + int full_design; /* in uAh */ + int full_last; /* in uAh */ + int remaining; /* in uAh */ + int present_rate; /* in uA, always non-negative */ + + /* derived properties */ int seconds_remaining; float percentage_remaining; charging_status_t status; }; +#if defined(LINUX) || defined(__NetBSD__) +/* + * Add batt_info data to acc. + */ +static void add_battery_info(struct battery_info *acc, const struct battery_info *batt_info) { + if (acc->remaining < 0) { + /* initialize accumulator so we can add to it */ + acc->full_design = 0; + acc->full_last = 0; + acc->remaining = 0; + acc->present_rate = 0; + } + + acc->full_design += batt_info->full_design; + acc->full_last += batt_info->full_last; + acc->remaining += batt_info->remaining; + + /* make present_rate negative for discharging and positive for charging */ + int present_rate = (acc->status == CS_DISCHARGING ? -1 : 1) * acc->present_rate; + present_rate += (batt_info->status == CS_DISCHARGING ? -1 : 1) * batt_info->present_rate; + + /* merge status */ + switch (acc->status) { + case CS_UNKNOWN: + acc->status = batt_info->status; + break; + + case CS_DISCHARGING: + if (present_rate > 0) + acc->status = CS_CHARGING; + /* else if batt_info is DISCHARGING: no conflict + * else if batt_info is CHARGING: present_rate should indicate that + * else if batt_info is FULL: but something else is discharging */ + break; + + case CS_CHARGING: + if (present_rate < 0) + acc->status = CS_DISCHARGING; + /* else if batt_info is DISCHARGING: present_rate should indicate that + * else if batt_info is CHARGING: no conflict + * else if batt_info is FULL: but something else is charging */ + break; + + case CS_FULL: + if (batt_info->status != CS_UNKNOWN) + acc->status = batt_info->status; + /* else: retain FULL, since it is more specific than UNKNOWN */ + break; + } + + acc->present_rate = abs(present_rate); +} +#endif + static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen, char *buffer, int number, const char *path, const char *format_down) { char *outwalk = buffer; @@ -185,12 +255,9 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen /* * Using envsys(4) via sysmon(4). */ - bool watt_as_unit = false; - int voltage = -1; int fd, rval; bool is_found = false; - char *sensor_desc; - bool is_full = false; + char sensor_desc[16]; prop_dictionary_t dict; prop_array_t array; @@ -198,7 +265,8 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen prop_object_iterator_t iter2; prop_object_t obj, obj2, obj3, obj4, obj5; - asprintf(&sensor_desc, "acpibat%d", number); + if (number >= 0) + (void)snprintf(sensor_desc, sizeof(sensor_desc), "acpibat%d", number); fd = open("/dev/sysmon", O_RDONLY); if (fd < 0) { @@ -227,9 +295,17 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen /* iterate over the dictionary returned by the kernel */ while ((obj = prop_object_iterator_next(iter)) != NULL) { /* skip this dict if it's not what we're looking for */ - if (strcmp(sensor_desc, - prop_dictionary_keysym_cstring_nocopy(obj)) != 0) - continue; + if (number < 0) { + /* we want all batteries */ + if (!BEGINS_WITH(prop_dictionary_keysym_cstring_nocopy(obj), + "acpibat")) + continue; + } else { + /* we want a specific battery */ + if (strcmp(sensor_desc, + prop_dictionary_keysym_cstring_nocopy(obj)) != 0) + continue; + } is_found = true; @@ -249,6 +325,16 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen return false; } + struct battery_info batt_buf = { + .full_design = 0, + .full_last = 0, + .remaining = 0, + .present_rate = 0, + .status = CS_UNKNOWN, + }; + int voltage = -1; + bool watt_as_unit = false; + /* iterate over array of dicts specific to target battery */ while ((obj2 = prop_object_iterator_next(iter2)) != NULL) { obj3 = prop_dictionary_get(obj2, "description"); @@ -260,19 +346,16 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen obj3 = prop_dictionary_get(obj2, "cur-value"); if (prop_number_integer_value(obj3)) - batt_info->status = CS_CHARGING; + batt_buf.status = CS_CHARGING; else - batt_info->status = CS_DISCHARGING; + batt_buf.status = CS_DISCHARGING; } else if (strcmp("charge", prop_string_cstring_nocopy(obj3)) == 0) { obj3 = prop_dictionary_get(obj2, "cur-value"); obj4 = prop_dictionary_get(obj2, "max-value"); obj5 = prop_dictionary_get(obj2, "type"); - batt_info->remaining = prop_number_integer_value(obj3); - batt_info->full_design = prop_number_integer_value(obj4); - - if (batt_info->remaining == batt_info->full_design) - is_full = true; + batt_buf.remaining = prop_number_integer_value(obj3); + batt_buf.full_design = prop_number_integer_value(obj4); if (strcmp("Ampere hour", prop_string_cstring_nocopy(obj5)) == 0) watt_as_unit = false; @@ -280,19 +363,31 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen watt_as_unit = true; } else if (strcmp("discharge rate", prop_string_cstring_nocopy(obj3)) == 0) { obj3 = prop_dictionary_get(obj2, "cur-value"); - batt_info->present_rate = prop_number_integer_value(obj3); + batt_buf.present_rate = prop_number_integer_value(obj3); } else if (strcmp("charge rate", prop_string_cstring_nocopy(obj3)) == 0) { obj3 = prop_dictionary_get(obj2, "cur-value"); batt_info->present_rate = prop_number_integer_value(obj3); } else if (strcmp("last full cap", prop_string_cstring_nocopy(obj3)) == 0) { obj3 = prop_dictionary_get(obj2, "cur-value"); - batt_info->full_last = prop_number_integer_value(obj3); + batt_buf.full_last = prop_number_integer_value(obj3); } else if (strcmp("voltage", prop_string_cstring_nocopy(obj3)) == 0) { obj3 = prop_dictionary_get(obj2, "cur-value"); voltage = prop_number_integer_value(obj3); } } prop_object_iterator_release(iter2); + + if (!watt_as_unit && voltage != -1) { + batt_buf.present_rate = (((float)voltage / 1000.0) * ((float)batt_buf.present_rate / 1000.0)); + batt_buf.remaining = (((float)voltage / 1000.0) * ((float)batt_buf.remaining / 1000.0)); + batt_buf.full_design = (((float)voltage / 1000.0) * ((float)batt_buf.full_design / 1000.0)); + batt_buf.full_last = (((float)voltage / 1000.0) * ((float)batt_buf.full_last / 1000.0)); + } + + if (batt_buf.remaining == batt_buf.full_design) + batt_buf.status = CS_FULL; + + add_battery_info(batt_info, &batt_buf); } prop_object_iterator_release(iter); @@ -304,15 +399,67 @@ static bool slurp_battery_info(struct battery_info *batt_info, yajl_gen json_gen return false; } - if (!watt_as_unit && voltage != -1) { - batt_info->present_rate = (((float)voltage / 1000.0) * ((float)batt_info->present_rate / 1000.0)); - batt_info->remaining = (((float)voltage / 1000.0) * ((float)batt_info->remaining / 1000.0)); - batt_info->full_design = (((float)voltage / 1000.0) * ((float)batt_info->full_design / 1000.0)); - batt_info->full_last = (((float)voltage / 1000.0) * ((float)batt_info->full_last / 1000.0)); + batt_info->present_rate = abs(batt_info->present_rate); +#endif + + return true; +} + +/* + * Populate batt_info with aggregate information about all batteries. + * Returns false on error, and an error message will have been written. + */ +static bool slurp_all_batteries(struct battery_info *batt_info, yajl_gen json_gen, char *buffer, const char *path, const char *format_down) { +#if defined(LINUX) + char *outwalk = buffer; + bool is_found = false; + + /* 1,000 batteries should be enough for anyone */ + for (int i = 0; i < 1000; i++) { + char batpath[1024]; + (void)snprintf(batpath, sizeof(batpath), path, i); + + if (!strcmp(batpath, path)) { + OUTPUT_FULL_TEXT("no '%d' in battery path"); + return false; + } + + /* Probe to see if there is such a battery. */ + struct stat sb; + if (stat(batpath, &sb) != 0) { + /* No such file, then we are done, assuming sysfs files have sequential numbers. */ + if (errno == ENOENT) + break; + + OUTPUT_FULL_TEXT(format_down); + return false; + } + + struct battery_info batt_buf = { + .full_design = 0, + .full_last = 0, + .remaining = 0, + .present_rate = 0, + .status = CS_UNKNOWN, + }; + if (!slurp_battery_info(&batt_buf, json_gen, buffer, i, path, format_down)) + return false; + + is_found = true; + add_battery_info(batt_info, &batt_buf); } - if (is_full) - batt_info->status = CS_FULL; + if (!is_found) { + OUTPUT_FULL_TEXT(format_down); + return false; + } + + batt_info->present_rate = abs(batt_info->present_rate); +#else + /* FreeBSD and OpenBSD only report aggregates. NetBSD always + * iterates through all batteries, so it's more efficient to + * aggregate in slurp_battery_info. */ + return slurp_battery_info(batt_info, json_gen, buffer, -1, path, format_down); #endif return true; @@ -324,10 +471,11 @@ void print_battery_info(yajl_gen json_gen, char *buffer, int number, const char struct battery_info batt_info = { .full_design = -1, .full_last = -1, + .remaining = -1, .present_rate = -1, .seconds_remaining = -1, .percentage_remaining = -1, - .status = CS_DISCHARGING, + .status = CS_UNKNOWN, }; bool colorful_output = false; @@ -340,8 +488,13 @@ void print_battery_info(yajl_gen json_gen, char *buffer, int number, const char hide_seconds = true; #endif - if (!slurp_battery_info(&batt_info, json_gen, buffer, number, path, format_down)) - return; + if (number < 0) { + if (!slurp_all_batteries(&batt_info, json_gen, buffer, path, format_down)) + return; + } else { + if (!slurp_battery_info(&batt_info, json_gen, buffer, number, path, format_down)) + return; + } int full = (last_full_capacity ? batt_info.full_last : batt_info.full_design); if (full < 0 && batt_info.percentage_remaining < 0) {