Add generic UpDeviceBattery base class

This class can handle laptop battery related quirks and estimations.
This commit is contained in:
Benjamin Berg 2022-06-07 16:20:31 +02:00
parent c6cd9beff3
commit d672eb1d90
4 changed files with 577 additions and 0 deletions

View file

@ -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',

View file

@ -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 */

481
src/up-device-battery.c Normal file
View file

@ -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;
}

93
src/up-device-battery.h Normal file
View file

@ -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