summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenjamin Berg <bberg@redhat.com>2022-06-07 16:20:31 +0200
committerBenjamin Berg <bberg@redhat.com>2022-06-21 11:15:57 +0200
commitd672eb1d908bcd7534ac325421f7841bd1dfec15 (patch)
treea4444b2ebfcf22f191cd5d8db0b5438a19feeae4
parentc6cd9beff30588be711c3dd22d30f0496b210e1f (diff)
downloadupower-d672eb1d908bcd7534ac325421f7841bd1dfec15.tar.gz
Add generic UpDeviceBattery base class
This class can handle laptop battery related quirks and estimations.
-rw-r--r--src/meson.build2
-rw-r--r--src/up-constants.h1
-rw-r--r--src/up-device-battery.c481
-rw-r--r--src/up-device-battery.h93
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