diff --git a/dbus/org.freedesktop.UPower.Device.xml b/dbus/org.freedesktop.UPower.Device.xml
index cbeccd2..e8606d5 100644
--- a/dbus/org.freedesktop.UPower.Device.xml
+++ b/dbus/org.freedesktop.UPower.Device.xml
@@ -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
+
+
+
+
+ Number of seconds until the CriticalPowerAction is triggered.
+ Is set to 0 if unknown.
+
+ This property is only valid if the property
+ type
+ has the value "battery" and the device is "/org/freedesktop/UPower/devices/DisplayDevice".
+
+
+
+
+
diff --git a/libupower-glib/up-device.c b/libupower-glib/up-device.c
index 1773f1d..bd5a651 100644
--- a/libupower-glib/up-device.c
+++ b/libupower-glib/up-device.c
@@ -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:
*
diff --git a/src/linux/integration-test.py b/src/linux/integration-test.py
index 9ceb582..cac2363 100755
--- a/src/linux/integration-test.py
+++ b/src/linux/integration-test.py
@@ -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
#
diff --git a/src/linux/up-device-supply.c b/src/linux/up-device-supply.c
index ae4e696..31bab63 100644
--- a/src/linux/up-device-supply.c
+++ b/src/linux/up-device-supply.c
@@ -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,
diff --git a/src/up-daemon.c b/src/up-daemon.c
index 38d49d5..e44fc92 100644
--- a/src/up-daemon.c
+++ b/src/up-daemon.c
@@ -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,