Merge branch 'wip/mafm/property-time-to-action' into 'master'

New property for time estimate to CriticalPowerAction (time-to-action)

See merge request upower/upower!291
This commit is contained in:
Manuel A. Fernandez Montecelo 2026-03-17 14:01:52 +00:00
commit 4eca33db21
5 changed files with 221 additions and 2 deletions

View file

@ -97,6 +97,10 @@ method return sender=:1.386 -> dest=:1.477 reply_serial=2
string "time-to-empty"
variant int64 0
)
dict entry(
string "time-to-action"
variant int64 0
)
dict entry(
string "time-to-full"
variant int64 0
@ -603,6 +607,21 @@ method return sender=:1.386 -> dest=:1.477 reply_serial=2
</doc:doc>
</property>
<property name="TimeToAction" type="x" access="read">
<doc:doc>
<doc:description>
<doc:para>
Number of seconds until the CriticalPowerAction is triggered.
Is set to 0 if unknown.
</doc:para><doc:para>
This property is only valid if the property
<doc:ref type="property" to="Source:Type">type</doc:ref>
has the value "battery" and the device is "/org/freedesktop/UPower/devices/DisplayDevice".
</doc:para>
</doc:description>
</doc:doc>
</property>
<property name="TimeToFull" type="x" access="read">
<doc:doc>
<doc:description>

View file

@ -83,6 +83,7 @@ enum {
PROP_VOLTAGE,
PROP_LUMINOSITY,
PROP_TIME_TO_EMPTY,
PROP_TIME_TO_ACTION,
PROP_TIME_TO_FULL,
PROP_PERCENTAGE,
PROP_TEMPERATURE,
@ -390,6 +391,11 @@ up_device_to_text (UpDevice *device)
g_string_append_printf (string, " time to full: %s\n", time_str);
g_free (time_str);
}
if (up_exported_device_get_time_to_action (priv->proxy_device) > 0) {
time_str = up_device_to_text_time_to_string (up_exported_device_get_time_to_action (priv->proxy_device));
g_string_append_printf (string, " time to action: %s\n", time_str);
g_free (time_str);
}
if (up_exported_device_get_time_to_empty (priv->proxy_device) > 0) {
time_str = up_device_to_text_time_to_string (up_exported_device_get_time_to_empty (priv->proxy_device));
g_string_append_printf (string, " time to empty: %s\n", time_str);
@ -624,6 +630,7 @@ static void
up_device_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
UpDevice *device = UP_DEVICE (object);
gboolean is_display = FALSE;
if (device->priv->proxy_device == NULL) {
GValue *v;
@ -707,6 +714,12 @@ up_device_set_property (GObject *object, guint prop_id, const GValue *value, GPa
case PROP_TIME_TO_EMPTY:
up_exported_device_set_time_to_empty (device->priv->proxy_device, g_value_get_int64 (value));
break;
case PROP_TIME_TO_ACTION:
/* set this property only for DisplayDevice */
is_display = (g_strcmp0 ("/org/freedesktop/UPower/devices/DisplayDevice", up_device_get_object_path (device)) == 0);
if (is_display)
up_exported_device_set_time_to_action (device->priv->proxy_device, g_value_get_int64 (value));
break;
case PROP_TIME_TO_FULL:
up_exported_device_set_time_to_full (device->priv->proxy_device, g_value_get_int64 (value));
break;
@ -857,6 +870,9 @@ up_device_get_property (GObject *object, guint prop_id, GValue *value, GParamSpe
case PROP_TIME_TO_EMPTY:
g_value_set_int64 (value, up_exported_device_get_time_to_empty (device->priv->proxy_device));
break;
case PROP_TIME_TO_ACTION:
g_value_set_int64 (value, up_exported_device_get_time_to_action (device->priv->proxy_device));
break;
case PROP_TIME_TO_FULL:
g_value_set_int64 (value, up_exported_device_get_time_to_full (device->priv->proxy_device));
break;
@ -1225,6 +1241,18 @@ up_device_class_init (UpDeviceClass *klass)
g_param_spec_int64 ("time-to-empty", NULL, NULL,
0, G_MAXINT64, 0,
G_PARAM_READWRITE));
/**
* UpDevice:time-to-action:
*
* The amount of time until the CriticalPowerAction is triggered.
*
* Since: 1.90.11
**/
g_object_class_install_property (object_class,
PROP_TIME_TO_ACTION,
g_param_spec_int64 ("time-to-action", NULL, NULL,
0, G_MAXINT64, 0,
G_PARAM_READWRITE));
/**
* UpDevice:time-to-full:
*

View file

@ -6178,6 +6178,168 @@ class Tests(dbusmock.DBusTestCase):
os.rmdir(conf_d_dir_path)
os.rmdir(base_dir.name)
def test_time_to_action(self):
"""Test time-to-action property with different configurations to ensure calculations work as expected"""
def inner_helper_check_general_and_energy():
# get devices
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
bat0_up = devs[0]
# check general state
self.assertDevs({"battery_BAT0": {"State": UP_DEVICE_STATE_DISCHARGING}})
self.assertEqual(
self.get_dbus_display_property("State"), UP_DEVICE_STATE_DISCHARGING
)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.assertEqual(self.get_dbus_display_property("IsPresent"), True)
# energy is in uWh, charge_* is in uAh, energy=charge*voltage
self.assertAlmostEqual(self.get_dbus_dev_property(bat0_up, "Voltage"), 12.0)
self.assertAlmostEqual(
self.get_dbus_dev_property(bat0_up, "EnergyFullDesign"), 132.0
)
self.assertAlmostEqual(
self.get_dbus_display_property("EnergyFullDesign"), 0.0
)
self.assertAlmostEqual(self.get_dbus_display_property("EnergyFull"), 120.0)
self.assertAlmostEqual(self.get_dbus_display_property("Energy"), 96.0)
self.assertAlmostEqual(self.get_dbus_display_property("Percentage"), 80.0)
self.assertAlmostEqual(self.get_dbus_display_property("EnergyRate"), 24.0)
# (96/24)*3600, 4h in seconds
self.assertAlmostEqual(self.get_dbus_display_property("TimeToEmpty"), 14400)
def inner_helper_check_log_line(check_string):
self.daemon_log.check_line(
check_string, timeout=UP_DAEMON_ACTION_DELAY + 0.5
)
self.testbed.add_device(
"power_supply",
"BAT0",
None,
[
"type",
"Battery",
"present",
"1",
"status",
"Discharging",
"charge_now",
"8000000",
"charge_full",
"10000000",
"charge_full_design",
"11000000",
"percentage",
"80",
"voltage_now",
"12000000",
"current_now",
"2000000",
],
[],
)
base_dir = tempfile.TemporaryDirectory(delete=False, prefix="UPower-")
# UPower.conf.d dir is mandaory
conf_d_dir_path = os.path.join(base_dir.name, "UPower.conf.d")
conf_d_dir = os.mkdir(path=conf_d_dir_path)
# Combination of {Percentage,Time}{Low,Critical,Action} are all needed
# in the "main config file" to avoid falling back to defaults, and for
# the config fragments to be able to override and work as expected (they
# keys in config fragments are assumed valid if, and only if, they have
# been seen previously in the main config file)
config = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=base_dir.name, suffix=".conf"
)
config.write("[UPower]\n")
config.write("AllowRiskyCriticalPowerAction=true\n")
config.write("CriticalPowerAction=Ignore\n")
config.write("UsePercentageForPolicy=true\n")
config.write("PercentageLow=15.0\n")
config.write("PercentageCritical=10.0\n")
config.write("PercentageAction=5.0\n")
config.write("TimeLow=300\n")
config.write("TimeCritical=200\n")
config.write("TimeAction=100\n")
config.close()
###############################
# Test #1, PercentageAction=5.0
###############################
self.start_daemon(cfgfile=config.name, warns=True)
# check warning message to ensure that we're using the expected config:
# AllowRiskyCriticalPowerAction=true and CriticalPowerAction=Ignore
inner_helper_check_log_line(
'The "Ignore" CriticalPowerAction setting is considered risky:'
)
inner_helper_check_general_and_energy()
# specific time-to-action test: 4h-5%, in seconds
self.assertAlmostEqual(self.get_dbus_display_property("TimeToAction"), 13680)
self.stop_daemon()
# Time-based instead of percentage-based, overriding main config
config2 = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=conf_d_dir_path, prefix="30-", suffix=".conf"
)
config2.write("[UPower]\n")
config2.write("AllowRiskyCriticalPowerAction=true\n")
config2.write("CriticalPowerAction=Suspend\n")
config2.write("UsePercentageForPolicy=false\n")
config2.write("TimeAction=60\n")
config2.close()
###############################
# Test #2, TimeAction=60
###############################
self.start_daemon(cfgfile=config.name, warns=True)
# check warning message to ensure that we're using the expected config:
# AllowRiskyCriticalPowerAction=true and CriticalPowerAction=Suspend
inner_helper_check_log_line(
'The "Suspend" CriticalPowerAction setting is considered risky:'
)
inner_helper_check_general_and_energy()
# specific time-to-action test: 4h-1min, in seconds
self.assertAlmostEqual(self.get_dbus_display_property("TimeToAction"), 14340)
self.stop_daemon()
# Percentage-based again, overriding both main config and lower-prio
# config fragments
config3 = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=conf_d_dir_path, prefix="60-", suffix=".conf"
)
config3.write("[UPower]\n")
config3.write("AllowRiskyCriticalPowerAction=false\n")
config3.write("CriticalPowerAction=Ignore\n")
config3.write("UsePercentageForPolicy=true\n")
config3.write("PercentageAction=1.0\n")
config3.close()
###############################
# Test #3, PercentageAction=1.0
###############################
self.start_daemon(cfgfile=config.name, warns=True)
# check warning message to ensure that we're using the expected config:
# AllowRiskyCriticalPowerAction=false and CriticalPowerAction=Ignore
inner_helper_check_log_line('The system will perform "HybridSleep" instead.')
inner_helper_check_general_and_energy()
# specific time-to-action test: 4h-1%, in seconds
self.assertAlmostEqual(self.get_dbus_display_property("TimeToAction"), 14256)
self.stop_daemon()
# final cleanup
os.unlink(config.name)
os.unlink(config2.name)
os.unlink(config3.name)
os.rmdir(conf_d_dir_path)
os.rmdir(base_dir.name)
#
# Helper methods
#

View file

@ -102,6 +102,7 @@ up_device_supply_reset_values (UpDeviceSupply *supply)
"energy-rate", (gdouble) 0.0,
"voltage", (gdouble) 0.0,
"time-to-empty", (gint64) 0,
"time-to-action", (gint64) 0,
"time-to-full", (gint64) 0,
"percentage", (gdouble) 0.0,
"temperature", (gdouble) 0.0,

View file

@ -61,6 +61,7 @@ struct UpDaemonPrivate
gdouble energy_full;
gdouble energy_rate;
gint64 time_to_empty;
gint64 time_to_action;
gint64 time_to_full;
gboolean charge_threshold_enabled;
@ -150,6 +151,7 @@ up_daemon_update_display_battery (UpDaemon *daemon)
gdouble energy_full_total = 0.0;
gdouble energy_rate_total = 0.0;
gint64 time_to_empty_total = 0;
gint64 time_to_action_total = 0;
gint64 time_to_full_total = 0;
gboolean is_present_total = FALSE;
gboolean charge_threshold_enabled_total = FALSE;
@ -309,9 +311,13 @@ out:
/* calculate a quick and dirty time remaining value
* NOTE: Keep in sync with per-battery estimation code! */
if (energy_rate_total > 0) {
if (state_total == UP_DEVICE_STATE_DISCHARGING)
if (state_total == UP_DEVICE_STATE_DISCHARGING) {
time_to_empty_total = SECONDS_PER_HOUR * (energy_total / energy_rate_total);
else if (state_total == UP_DEVICE_STATE_CHARGING)
if (daemon->priv->use_percentage_for_policy)
time_to_action_total = time_to_empty_total * (1.0 - (daemon->priv->action_percentage / 100.0));
else
time_to_action_total = time_to_empty_total - daemon->priv->action_time;
} else if (state_total == UP_DEVICE_STATE_CHARGING)
time_to_full_total = SECONDS_PER_HOUR * ((energy_full_total - energy_total) / energy_rate_total);
}
@ -325,6 +331,7 @@ out:
daemon->priv->energy_full == energy_full_total &&
daemon->priv->energy_rate == energy_rate_total &&
daemon->priv->time_to_empty == time_to_empty_total &&
daemon->priv->time_to_action == time_to_action_total &&
daemon->priv->time_to_full == time_to_full_total &&
daemon->priv->percentage == percentage_total &&
daemon->priv->charge_threshold_enabled == charge_threshold_enabled_total &&
@ -337,6 +344,7 @@ out:
daemon->priv->energy_full = energy_full_total;
daemon->priv->energy_rate = energy_rate_total;
daemon->priv->time_to_empty = time_to_empty_total;
daemon->priv->time_to_action = time_to_action_total;
daemon->priv->time_to_full = time_to_full_total;
daemon->priv->percentage = percentage_total;
@ -351,6 +359,7 @@ out:
"energy-full", energy_full_total,
"energy-rate", energy_rate_total,
"time-to-empty", time_to_empty_total,
"time-to-action", time_to_action_total,
"time-to-full", time_to_full_total,
"percentage", percentage_total,
"is-present", is_present_total,