diff options
author | Benjamin Berg <bberg@redhat.com> | 2022-06-07 16:20:31 +0200 |
---|---|---|
committer | Benjamin Berg <bberg@redhat.com> | 2022-06-21 11:15:57 +0200 |
commit | d672eb1d908bcd7534ac325421f7841bd1dfec15 (patch) | |
tree | a4444b2ebfcf22f191cd5d8db0b5438a19feeae4 | |
parent | c6cd9beff30588be711c3dd22d30f0496b210e1f (diff) | |
download | upower-d672eb1d908bcd7534ac325421f7841bd1dfec15.tar.gz |
Add generic UpDeviceBattery base class
This class can handle laptop battery related quirks and estimations.
-rw-r--r-- | src/meson.build | 2 | ||||
-rw-r--r-- | src/up-constants.h | 1 | ||||
-rw-r--r-- | src/up-device-battery.c | 481 | ||||
-rw-r--r-- | src/up-device-battery.h | 93 |
4 files changed, 577 insertions, 0 deletions
diff --git a/src/meson.build b/src/meson.build index f541f4c..477bb0c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -31,6 +31,8 @@ upowerd_private = static_library('upowerd-private', 'up-daemon.c', 'up-device.h', 'up-device.c', + 'up-device-battery.h', + 'up-device-battery.c', 'up-device-list.h', 'up-device-list.c', 'up-enumerator.c', diff --git a/src/up-constants.h b/src/up-constants.h index 33324c5..660e7fd 100644 --- a/src/up-constants.h +++ b/src/up-constants.h @@ -28,6 +28,7 @@ G_BEGIN_DECLS #define UP_DAEMON_UNKNOWN_TIMEOUT 1 /* second */ #define UP_DAEMON_UNKNOWN_POLL_TIME 5 /* second */ +#define UP_DAEMON_ESTIMATE_TIMEOUT 5 /* second */ #define UP_DAEMON_SHORT_TIMEOUT 30 /* seconds */ #define UP_DAEMON_LONG_TIMEOUT 120 /* seconds */ diff --git a/src/up-device-battery.c b/src/up-device-battery.c new file mode 100644 index 0000000..2300568 --- /dev/null +++ b/src/up-device-battery.c @@ -0,0 +1,481 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2022 Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include <string.h> + +#include "up-constants.h" +#include "up-config.h" +#include "up-device-battery.h" + +/* Chosen to be quite big, in case there was a lot of re-polling */ +#define MAX_ESTIMATION_POINTS 15 + +typedef struct { + UpBatteryValues hw_data[MAX_ESTIMATION_POINTS]; + gint hw_data_last; + gint hw_data_len; + + gboolean present; + gboolean units_changed_warning; + + /* static values (only changed if plugged/unplugged) */ + gboolean disable_battery_poll; + gdouble voltage_design; + UpBatteryUnits units; + + /* mostly static values */ + gdouble energy_full; + gdouble energy_full_reported; + gdouble energy_design; + gint charge_cycles; + + /* dynamic values */ + gint64 fast_repoll_until; + gboolean have_good_estimates; +} UpDeviceBatteryPrivate; + +G_DEFINE_TYPE_EXTENDED (UpDeviceBattery, up_device_battery, UP_TYPE_DEVICE, 0, + G_ADD_PRIVATE (UpDeviceBattery)) + +static gboolean +up_device_battery_get_on_battery (UpDevice *device, gboolean *on_battery) +{ + UpDeviceState state; + + g_return_val_if_fail (on_battery != NULL, FALSE); + + g_object_get (device, + "state", &state, + NULL); + + *on_battery = (state == UP_DEVICE_STATE_DISCHARGING); + + return TRUE; +} + +static gdouble +up_device_battery_charge_to_energy (UpDeviceBattery *self, gdouble charge) +{ + UpDeviceBatteryPrivate *priv = up_device_battery_get_instance_private (self); + + /* We want to work with energy internally. + * Note that this is a pretty bad way of estimating the energy, + * we just assume that the voltage is always the same, which is + * obviously not true. The voltage depends on at least: + * - output current + * - temperature + * - charge + * The easiest way to improve this would likely be "machine learning", + * i.e. statistics through which we can calculate the actual + * performance based on the factors we have. + */ + return priv->voltage_design * charge; +} + +static void +up_device_battery_estimate (UpDeviceBattery *self) +{ + UpDeviceBatteryPrivate *priv = up_device_battery_get_instance_private (self); + UpBatteryValues *ref = NULL; + UpBatteryValues *cur; + gdouble energy_rate = 0.0; + gint64 ref_td = 999 * G_USEC_PER_SEC; /* We need to be able to do math with this */ + gint64 time_to_empty = 0; + gint64 time_to_full = 0; + gint i; + + g_assert (priv->hw_data_len >= 1); + + priv->have_good_estimates = FALSE; + + cur = &priv->hw_data[priv->hw_data_last]; + if (cur->state != UP_DEVICE_STATE_CHARGING && cur->state != UP_DEVICE_STATE_DISCHARGING) { + priv->have_good_estimates = TRUE; + goto out; + } + + for (i = 1; i < priv->hw_data_len; i++) { + int pos = (priv->hw_data_last - i + G_N_ELEMENTS (priv->hw_data)) % G_N_ELEMENTS (priv->hw_data); + gint64 td; + + /* Stop searching if the state changed. */ + if (priv->hw_data[pos].state != cur->state) + break; + + td = cur->ts_us - priv->hw_data[pos].ts_us; + /* At least 15 seconds worth of data. */ + if (td < 15 * G_USEC_PER_SEC) + continue; + + /* Stop searching if the new reference is further away from the long timeout. */ + if (abs(UP_DAEMON_LONG_TIMEOUT * G_USEC_PER_SEC - abs (td)) > abs(UP_DAEMON_SHORT_TIMEOUT * G_USEC_PER_SEC - ref_td)) + break; + + ref_td = td; + ref = &priv->hw_data[pos]; + } + + /* We rely solely on battery reports here, with dynamic power + * usage (in particular during resume), lets just wait for a + * bit longer before reporting anything to the user. + * + * Alternatively, we could assume that some old estimate for the + * energy rate remains stable and do a time estimate based on that. + * + * For now, this is better than what we used to do. + */ + if (!ref) + goto out; + + /* energy is in Wh, rate in W */ + energy_rate = (cur->energy.cur - ref->energy.cur) / (ref_td / ((gdouble) 3600 * G_USEC_PER_SEC)); + + /* The rate is defined to be positive during both charge and discharge. */ + if (cur->state == UP_DEVICE_STATE_DISCHARGING) + energy_rate *= -1.0; + + /* This hopefully gives us sane values, but lets print a message if not. */ + if (energy_rate < 0.1 || energy_rate > 300) { + g_message ("The estimated %scharge rate is %fW, which is not realistic", + cur->state == UP_DEVICE_STATE_DISCHARGING ? "dis" : "", + energy_rate); + energy_rate = 0; + goto out; + } + + /* Here we could factor in collected data about charge rates */ + /* FIXME: Use charge-stop-threshold here */ + if (cur->state == UP_DEVICE_STATE_CHARGING) + time_to_full = 3600 * (priv->energy_full - cur->energy.cur) / energy_rate; + else + time_to_empty = 3600 * cur->energy.cur / energy_rate; + + priv->have_good_estimates = TRUE; + +out: + g_object_set (self, + "energy-rate", energy_rate, + "time-to-empty", time_to_empty, + "time-to-full", time_to_full, + NULL); +} + +static void +up_device_battery_update_poll_frequency (UpDeviceBattery *self, + UpDeviceState state, + UpRefreshReason reason) +{ + UpDeviceBatteryPrivate *priv = up_device_battery_get_instance_private (self); + gint slow_poll_timeout; + + if (priv->disable_battery_poll) + return; + + slow_poll_timeout = priv->have_good_estimates ? UP_DAEMON_SHORT_TIMEOUT : UP_DAEMON_ESTIMATE_TIMEOUT; + + /* We start fast-polling if the reason to update was not a normal POLL + * and one of the following holds true: + * 1. The current stat is unknown; we hope that this is transient + * and re-poll. + * 2. A change occured on a line power supply. This likely means that + * batteries switch between charging/discharging which does not + * always result in a separate uevent. + * + * For simplicity, we do the fast polling for a specific period of time. + * If the reason to do fast-polling was an unknown state, then it would + * also be reasonable to stop as soon as we got a proper state. + */ + if (reason != UP_REFRESH_POLL && + (state == UP_DEVICE_STATE_UNKNOWN || + reason == UP_REFRESH_LINE_POWER)) { + g_debug ("unknown_poll: setting up fast re-poll"); + g_object_set (self, "poll-timeout", UP_DAEMON_UNKNOWN_TIMEOUT, NULL); + priv->fast_repoll_until = g_get_monotonic_time () + UP_DAEMON_UNKNOWN_POLL_TIME * G_USEC_PER_SEC; + + } else if (priv->fast_repoll_until == 0) { + /* Not fast-repolling, check poll timeout is as expected */ + gint poll_timeout; + g_object_get (self, "poll-timeout", &poll_timeout, NULL); + if (poll_timeout != slow_poll_timeout) + g_object_set (self, "poll-timeout", slow_poll_timeout, NULL); + + } else if (priv->fast_repoll_until < g_get_monotonic_time ()) { + g_debug ("unknown_poll: stopping fast repoll (giving up)"); + priv->fast_repoll_until = 0; + g_object_set (self, "poll-timeout", slow_poll_timeout, NULL); + } +} + +void +up_device_battery_report (UpDeviceBattery *self, + UpBatteryValues *values, + UpRefreshReason reason) +{ + UpDeviceBatteryPrivate *priv = up_device_battery_get_instance_private (self); + + if (!priv->present) { + g_warning ("Got a battery report for a battery that is not present"); + return; + } + + /* Discard all old measurements (from before suspend). */ + if (reason == UP_REFRESH_RESUME) + priv->hw_data_len = 0; + + g_assert (priv->units != UP_BATTERY_UNIT_UNDEFINED); + + values->ts_us = g_get_monotonic_time (); + + /* QUIRK: + * + * There is an old bug where some Lenovo machine switched from reporting + * energy to reporting charge numbers. The code used to react by + * reloading everything, however, what apparently happens is that the + * *energy* value simply starts being reported through *charge* + * attributes. + * The original report is + * https://bugzilla.redhat.com/show_bug.cgi?id=587112 + * and inspecting the numbers it is clear that the values are + * really energy values that are unrealistically high as they get + * incorrectly multiplied by the voltage. + * + * Said differently, just assuming the units did *not* change should + * give us a saner value. Obviously, things will fall appart if upower + * is restarted and this should be fixed in the kernel or firmware. + * + * Unfortunately, the hardware is quite old (X201s) which makes it hard + * to even confirm that the bug was not fixed in the kernel or firmware. + * + * Note that a race condition could be the user swapping the battery + * during suspend and us re-polling energy data before noticing that + * the battery has changed. + */ + if (G_UNLIKELY (priv->units != values->units)) { + if (!priv->units_changed_warning) { + g_warning ("Battery unit type changed, assuming the old unit is still valid. This is likely a firmware or driver issue, please report!"); + priv->units_changed_warning = TRUE; + } + values->units = priv->units; + } + + if (values->units == UP_BATTERY_UNIT_CHARGE) { + values->units = UP_BATTERY_UNIT_ENERGY; + values->energy.cur = up_device_battery_charge_to_energy (self, values->charge.cur); + values->energy.rate = up_device_battery_charge_to_energy (self, values->charge.rate); + } + + /* QUIRK: Discard weird measurements (like a 300W power usage). */ + if (values->energy.rate > 300) + values->energy.rate = 0; + + /* Infer current energy if unknown */ + if (values->energy.cur < 0.01 && values->percentage > 0) + values->energy.cur = priv->energy_full * values->percentage / 100.0; + + /* QUIRK: Increase energy_full if energy.cur is higher */ + if (values->energy.cur > priv->energy_full) { + priv->energy_full = values->energy.cur; + g_object_set (self, + /* How healthy the battery is (clamp to 100% if it can hold more charge than expected) */ + "capacity", MIN (priv->energy_full / priv->energy_design * 100.0, 100), + "energy-full", priv->energy_full, + NULL); + } + + /* Infer percentage if unknown */ + if (values->percentage <= 0) + values->percentage = values->energy.cur / priv->energy_full * 100; + + /* QUIRK: Some devices keep reporting PENDING_CHARGE even when full */ + if (values->state == UP_DEVICE_STATE_PENDING_CHARGE && values->percentage >= UP_FULLY_CHARGED_THRESHOLD) + values->state = UP_DEVICE_STATE_FULLY_CHARGED; + + /* NOTE: We used to do more for the UNKNOWN state. However, some of the + * logic relies on only one battery device to be present. Plus, it + * requires knowing the AC state. + * Because of this complexity, the decision was made to only do this + * type of inferring inside the DisplayDevice. There we can be sure + * about the AC state and we only have "one" battery. + */ + + /* Push into our ring buffer */ + priv->hw_data_last = (priv->hw_data_last + 1) % G_N_ELEMENTS (priv->hw_data); + priv->hw_data_len = MIN (priv->hw_data_len + 1, G_N_ELEMENTS (priv->hw_data)); + priv->hw_data[priv->hw_data_last] = *values; + + /* Do estimations */ + up_device_battery_estimate (self); + + /* Set the main properties (setting "update-time" last) */ + g_object_set (self, + "energy", values->energy.cur, + "percentage", values->percentage, + "state", values->state, + "voltage", values->voltage, + "temperature", values->temperature, + /* XXX: Move "update-time" updates elsewhere? */ + "update-time", (guint64) g_get_real_time () / G_USEC_PER_SEC, + NULL); + + up_device_battery_update_poll_frequency (self, values->state, reason); +} + +void +up_device_battery_update_info (UpDeviceBattery *self, UpBatteryInfo *info) +{ + UpDeviceBatteryPrivate *priv = up_device_battery_get_instance_private (self); + + /* First, sanitize the information. */ + if (info->present && info->units == UP_BATTERY_UNIT_UNDEFINED) { + g_warning ("Battery without defined units, assuming unplugged"); + info->present = FALSE; + } + + + /* Still not present, ignore. */ + if (!info->present && !priv->present) + return; + + /* Emulate an unplug if present but vendor, etc. changed. */ + if (info->present && info->present == priv->present) { + g_autofree gchar *vendor = NULL; + g_autofree gchar *model = NULL; + g_autofree gchar *serial = NULL; + + g_object_get (self, + "vendor", &vendor, + "model", &model, + "serial", &serial, + NULL); + if (g_strcmp0 (vendor, info->vendor) != 0 || + g_strcmp0 (model, info->model) != 0 || + g_strcmp0 (serial, info->serial) != 0) { + UpBatteryInfo unplugged = { .present = FALSE }; + up_device_battery_update_info (self, &unplugged); + } + } + + if (info->present) { + gdouble energy_full; + gdouble energy_design; + gint charge_cycles; + + /* See above, we have a (new) battery plugged in. */ + if (!priv->present) { + g_object_set (self, + "is-present", TRUE, + "vendor", info->vendor, + "model", info->model, + "serial", info->serial, + "technology", info->technology, + "has-history", TRUE, + "has-statistics", TRUE, + NULL); + + /* FIXME: The history needs to be re-loaded at this + * point as the ID may have changed! + */ + priv->present = TRUE; + priv->units = info->units; + } + + /* See comment in up_device_battery_report */ + if (priv->units != info->units && !priv->units_changed_warning) { + g_warning ("Battery unit type changed, assuming the old unit is still valid. This is likely a firmware or driver issue, please report!"); + priv->units_changed_warning = TRUE; + } + + priv->voltage_design = info->voltage_design; + if (priv->units == UP_BATTERY_UNIT_CHARGE) { + energy_full = up_device_battery_charge_to_energy (self, info->charge.full); + energy_design = up_device_battery_charge_to_energy (self, info->charge.design); + } else { + energy_full = info->energy.full; + energy_design = info->energy.design; + } + + if (energy_full < 0.01) + energy_full = energy_design; + + /* Force -1 for unknown value (where 0 is also an unknown value) */ + charge_cycles = info->charge_cycles > 0 ? info->charge_cycles : -1; + + if (energy_full != priv->energy_full_reported || energy_design != priv->energy_design) { + priv->energy_full = energy_full; + priv->energy_full_reported = energy_full; + priv->energy_design = energy_design; + + g_object_set (self, + /* How healthy the battery is (clamp to 100% if it can hold more charge than expected) */ + "capacity", MIN (priv->energy_full / priv->energy_design * 100.0, 100), + "energy-full", priv->energy_full, + "energy-full-design", priv->energy_design, + NULL); + } + + if (priv->charge_cycles != charge_cycles) { + priv->charge_cycles = charge_cycles; + g_object_set (self, + "charge-cycles", charge_cycles, + NULL); + } + + /* NOTE: Assume a normal refresh will follow immediately (do not update timestamp). */ + } else { + priv->present = FALSE; + priv->hw_data_len = 0; + priv->units = UP_BATTERY_UNIT_UNDEFINED; + + g_object_set (self, + "is-present", FALSE, + "vendor", NULL, + "model", NULL, + "serial", NULL, + "technology", UP_DEVICE_TECHNOLOGY_UNKNOWN, + "capacity", (gdouble) 0.0, + "energy-full", (gdouble) 0.0, + "energy-full-design", (gdouble) 0.0, + "charge-cycles", -1, + "has-history", FALSE, + "has-statistics", FALSE, + "update-time", (guint64) g_get_real_time () / G_USEC_PER_SEC, + NULL); + } +} + + + +static void +up_device_battery_init (UpDeviceBattery *self) +{ + g_object_set (self, + "type", UP_DEVICE_KIND_BATTERY, + "power-supply", TRUE, + "is-rechargeable", TRUE, + NULL); +} + +static void +up_device_battery_class_init (UpDeviceBatteryClass *klass) +{ + UpDeviceClass *device_class = UP_DEVICE_CLASS (klass); + + device_class->get_on_battery = up_device_battery_get_on_battery; +} diff --git a/src/up-device-battery.h b/src/up-device-battery.h new file mode 100644 index 0000000..94cfcc9 --- /dev/null +++ b/src/up-device-battery.h @@ -0,0 +1,93 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2022 Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include "up-device.h" + +G_BEGIN_DECLS + +#define MAX_DISCHARGE_RATE 300 + +#define UP_TYPE_DEVICE_BATTERY (up_device_battery_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (UpDeviceBattery, up_device_battery, UP, DEVICE_BATTERY, UpDevice) + +struct _UpDeviceBatteryClass +{ + UpDeviceClass parent_class; +}; + +typedef enum { + UP_BATTERY_UNIT_UNDEFINED = 0, + UP_BATTERY_UNIT_ENERGY, + UP_BATTERY_UNIT_CHARGE, +} UpBatteryUnits; + +typedef struct { + gint64 ts_us; + UpDeviceState state; + UpBatteryUnits units; + + union { + struct { + gdouble cur; + gdouble rate; + } energy; + struct { + gdouble cur; + gdouble rate; + } charge; + }; + gdouble percentage; + gdouble voltage; + gdouble temperature; +} UpBatteryValues; + +typedef struct { + gboolean present; + + const char *vendor; + const char *model; + const char *serial; + + UpBatteryUnits units; + + union { + struct { + gdouble full; + gdouble design; + } energy; + struct { + gdouble full; + gdouble design; + } charge; + }; + + UpDeviceTechnology technology; + gdouble voltage_design; + gint charge_cycles; +} UpBatteryInfo; + + +void up_device_battery_update_info (UpDeviceBattery *self, UpBatteryInfo *info); +void up_device_battery_report (UpDeviceBattery *self, UpBatteryValues *values, UpRefreshReason reason); + +G_END_DECLS |