[neon/backports-jammy/power-profiles-daemon/Neon/unstable] /: 0.10.1-1 (patches unapplied)

git-ubuntu importer null at kde.org
Tue Sep 24 23:21:42 BST 2024


Git commit 6a5c5f03481ec3e41c0d98b40eb11068b0370626 by git-ubuntu importer, on behalf of Sebastien Bacher.
Committed on 08/11/2021 at 22:37.
Pushed by carlosdem into branch 'Neon/unstable'.

0.10.1-1 (patches unapplied)

Imported using git-ubuntu import.

A  +35   -0    .gitlab-ci.yml
M  +33   -0    NEWS
M  +46   -15   README.md
M  +15   -0    data/meson.build
A  +31   -0    data/net.hadess.PowerProfiles.policy
M  +3    -1    data/power-profiles-daemon.service.in
M  +10   -0    debian/changelog
M  +2    -0    debian/control
A  +29   -0    debian/patches/build_older_polkit.patch
M  +1    -0    debian/patches/series
M  +1    -0    docs/meson.build
M  +9    -6    meson.build
M  +4    -0    meson_options.txt
M  +9    -2    src/meson.build
M  +72   -8    src/net.hadess.PowerProfiles.xml
M  +406  -39   src/power-profiles-daemon.c
M  +116  -5    src/powerprofilesctl.in
M  +17   -15   src/ppd-driver-fake.c
M  +2    -3    src/ppd-driver-intel-pstate.c
M  +13   -17   src/ppd-driver-platform-profile.c
M  +33   -47   src/ppd-driver.c
M  +12   -13   src/ppd-driver.h
R  +305  -52   tests/integration-test.py [from: tests/integration-test - 058% similarity]
M  +14   -6    tests/meson.build
A  +46   -0    tests/unittest_inspector.py

https://invent.kde.org/neon/backports-jammy/power-profiles-daemon/-/commit/6a5c5f03481ec3e41c0d98b40eb11068b0370626

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..592a89d
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,35 @@
+image: fedora:rawhide
+
+variables:
+  DEPENDENCIES: gcc
+                gtk-doc
+                pkgconfig(udev)
+                pkgconfig(systemd)
+                pkgconfig(gio-2.0)
+                pkgconfig(gudev-1.0)
+                pkgconfig(upower-glib)
+                pkgconfig(polkit-gobject-1)
+                systemd
+                meson
+                git
+                python3-gobject
+                python3-dbusmock
+                python3-pylint
+                umockdev
+
+build_stable:
+  before_script:
+    - dnf upgrade -y --nogpgcheck fedora-release fedora-repos*
+    - dnf update -y && dnf install -y $DEPENDENCIES
+  script:
+    - meson -Dgtk_doc=true -Dpylint=true _build
+    - ninja -v -C _build
+    - ninja -v -C _build install
+    - ninja -v -C _build uninstall
+    - ninja -v -C _build dist
+    - meson test -C _build
+  artifacts:
+    when: always
+    paths:
+    - _build/meson-logs/*.txt
+    - _build/meson-dist/*
diff --git a/NEWS b/NEWS
index bc8a646..224ebbc 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,36 @@
+0.10.1
+------
+
+This release fixes a bug in the authorisation codepath added in 0.10.0, where holding
+a profile could still succeed despite having sent a denial to the calling process.
+
+This release also makes the pylint test optional. It should not be used unless the goal
+is to contribute patches to assuage it.
+
+0.10.0
+------
+
+This release adds authorisation checks for the profile holds and profile switching
+features of the backend daemon, through polkit. It is recommended that all
+distributions upgrade to this version as soon as possible.
+
+This release also adds support for the "quiet" kernel platform profile used
+in some systems.
+
+0.9.0
+-----
+
+This release adds support for "holding" a power profile while running a task
+or application, making it possible to switch to a performance profile during
+a compilation, or to a power-saver profile when low on battery, reverting to
+the original profile when done.
+
+This release also removes the "inhibited" property for the performance profile,
+which made it impossible to switch to that profile, and replaces it with the
+"degraded" property which lists why performance is degraded.
+
+Finally, the last used profile is now remembered across reboots.
+
 0.8.1
 -----
 
diff --git a/README.md b/README.md
index 768a744..99e102e 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Installation
 $ meson _build -Dprefix=/usr
 $ ninja -v -C _build install
 ```
-It requires libgudev and systemd.
+It requires libgudev, systemd and polkit-gobject.
 
 Introduction
 ------------
@@ -30,38 +30,47 @@ they are also expected to adjust the behaviour of the desktop depending on the m
 such as turning the screen off after inaction more aggressively when in power-saver
 mode.
 
-Note that power-profiles-daemon does not save the currently active profile across
-system restarts and will always start with the "balanced" profile selected.
-
 Debugging
 ---------
 
 You can now check which mode is in use, and which ones are available by running:
-```
-gdbus introspect --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles
+
+```sh
+powerprofilesctl
 ```
 
 You can change the selected profile by running (change `power-saver` for the
 chosen profile):
+
+```sh
+powerprofilesctl set power-saver
+```
+
+You can check the current configuration which will be restored on
+reboot in `/var/lib/power-profiles-daemon/state.ini`.
+
+Those commands are also available through the D-Bus interface:
+
 ```
+gdbus introspect --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles
 gdbus call --system --dest net.hadess.PowerProfiles --object-path /net/hadess/PowerProfiles --method org.freedesktop.DBus.Properties.Set 'net.hadess.PowerProfiles' 'ActiveProfile' "<'power-saver'>"
 ```
 
-If that doesn't work, please file an issue, make sure any running power-profiles-daemon
-has been stopped:
-`systemctl stop power-profiles-daemon.service`
-and attach the output of:
-`G_MESSAGES_DEBUG=all /usr/libexec/power-profiles-daemon`
-running as ```root```.
+If that doesn't work, please file an issue, attach the output of:
+
+```sh
+sudo G_MESSAGES_DEBUG=all /usr/libexec/power-profiles-daemon -r -v
+```
 
 Testing
 -------
 
-If you don't have hardware that can support the performance mode, you can
-manually run the `power-profiles-daemon` binary as `root` with the environment
+If you don't have hardware that can support the performance mode, or the degraded mode
+you can manually run the `power-profiles-daemon` binary as `root` with the environment
 variable `POWER_PROFILE_DAEMON_FAKE_DRIVER` set to 1. For example:
+
 ```sh
-$ sudo POWER_PROFILE_DAEMON_FAKE_DRIVER=1 /usr/libexec/power-profiles-daemon
+sudo POWER_PROFILE_DAEMON_FAKE_DRIVER=1 /usr/libexec/power-profiles-daemon -r -v
 ```
 
 References
@@ -152,3 +161,25 @@ addition to the "3 profiles" selection:
 
 A lot of those power-saving tricks could be analysed and used, but we
 obviously can't rely on "source available" software for our free desktops.
+
+### [system76-power](https://github.com/pop-os/system76-power)
+
+Very similar project to power-profiles-daemon but goes much more into the weeds
+in terms of power-saving/performance implementation.
+
+It has a D-Bus API for choosing different power profiles, and applies a number
+of settings based on the profile selected. Most of the interesting settings are
+already upstreamed ([SATA power tweaks](https://hansdegoede.livejournal.com/18412.html)),
+should be upstreamed to the vanilla kernel if possible (PCI power-savings), or
+are things we already implement (Intel P-State).
+
+It could without a doubt have been used as a base for power-profiles-daemon if it
+was more of an upstream project instead of a PopOS!/System76 project.
+
+### [asusctl](https://gitlab.com/asus-linux/asusctl/)
+
+It provides an interface to a number of ASUS-specific features which isn't directly
+relevant to power-profiles-daemon like handling keyboard LED settings, or setting
+battery charge limits. The functionality that was relevant got moved to the asus-wmi
+kernel driver during the 5.14 kernel development cycle, where power-profiles-daemon
+can consume it. The 2 daemons are now complementary.
diff --git a/data/meson.build b/data/meson.build
index 8904325..3d1b466 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -19,3 +19,18 @@ install_data(
   'net.hadess.PowerProfiles.service',
   install_dir: dbusservicedir
 )
+
+polkit_policy = 'net.hadess.PowerProfiles.policy'
+if xmllint.found()
+  test(polkit_policy,
+       xmllint,
+       args: [
+           '--noout',
+           meson.source_root() / 'data' / polkit_policy,
+       ])
+endif
+
+install_data(
+  polkit_policy,
+  install_dir: polkit_policy_directory,
+)
diff --git a/data/net.hadess.PowerProfiles.policy b/data/net.hadess.PowerProfiles.policy
new file mode 100644
index 0000000..a332b42
--- /dev/null
+++ b/data/net.hadess.PowerProfiles.policy
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
+
+<policyconfig>
+
+  <vendor>power-profiles-daemon</vendor>
+  <vendor_url>https://gitlab.freedesktop.org/hadess/power-profiles-daemon</vendor_url>
+
+  <action id="net.hadess.PowerProfiles.switch-profile">
+    <description>Switch Power Profile</description>
+    <message>Privileges are required to switch power profiles.</message>
+    <defaults>
+      <allow_any>no</allow_any>
+      <allow_inactive>no</allow_inactive>
+      <allow_active>yes</allow_active>
+    </defaults>
+  </action>
+
+  <action id="net.hadess.PowerProfiles.hold-profile">
+    <description>Hold Power Profile</description>
+    <message>Privileges are required to hold power profiles.</message>
+    <defaults>
+      <allow_any>no</allow_any>
+      <allow_inactive>no</allow_inactive>
+      <allow_active>yes</allow_active>
+    </defaults>
+  </action>
+
+</policyconfig>
diff --git a/data/power-profiles-daemon.service.in b/data/power-profiles-daemon.service.in
index 8fe56ce..dcd3503 100644
--- a/data/power-profiles-daemon.service.in
+++ b/data/power-profiles-daemon.service.in
@@ -1,6 +1,6 @@
 [Unit]
 Description=Power Profiles daemon
-Conflicts=tuned.service tlp.service auto-cpufreq.service
+Conflicts=tuned.service tlp.service auto-cpufreq.service system76-power.service
 Before=multi-user.target display-manager.target
 
 [Service]
@@ -8,6 +8,8 @@ Type=dbus
 BusName=net.hadess.PowerProfiles
 ExecStart=@libexecdir@/power-profiles-daemon
 Restart=on-failure
+# This always corresponds to /var/lib/power-profiles-daemon
+StateDirectory=power-profiles-daemon
 #Uncomment this to enable debug
 #Environment="G_MESSAGES_DEBUG=all"
 
diff --git a/debian/changelog b/debian/changelog
index 3155e44..3bae784 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,13 @@
+power-profiles-daemon (0.10.1-1) unstable; urgency=medium
+
+  * New upstream version
+  * debian/control:
+    - Build-Depends on libpolkit-gobject-1-dev and libxml2-utils
+  * debian/patches/build_older_polkit.patch:
+    - fix the build with older polkit versions
+
+ -- Sebastien Bacher <seb128 at ubuntu.com>  Mon, 08 Nov 2021 16:31:07 +0100
+
 power-profiles-daemon (0.8.1-1) experimental; urgency=medium
 
   * New upstream version
diff --git a/debian/control b/debian/control
index 757ab1c..b90929d 100644
--- a/debian/control
+++ b/debian/control
@@ -6,9 +6,11 @@ Uploaders: Sebastien Bacher <seb128 at ubuntu.com>
 Build-Depends: debhelper-compat (= 13),
                libglib2.0-dev,
                libgudev-1.0-dev,
+               libpolkit-gobject-1-dev,
                libupower-glib-dev,
                libudev-dev,
                libumockdev-dev,
+               libxml2-utils,
                meson,
                python3-dbus <!nocheck>,
                python3-dbusmock <!nocheck>,
diff --git a/debian/patches/build_older_polkit.patch b/debian/patches/build_older_polkit.patch
new file mode 100644
index 0000000..6cd50f2
--- /dev/null
+++ b/debian/patches/build_older_polkit.patch
@@ -0,0 +1,29 @@
+Index: power-profiles-daemon/src/power-profiles-daemon.c
+===================================================================
+--- power-profiles-daemon.orig/src/power-profiles-daemon.c
++++ power-profiles-daemon/src/power-profiles-daemon.c
+@@ -103,6 +103,13 @@ typedef enum {
+ 
+ #define PROP_ALL (PROP_ACTIVE_PROFILE | PROP_INHIBITED | PROP_PROFILES | PROP_ACTIONS | PROP_DEGRADED | PROP_ACTIVE_PROFILE_HOLDS)
+ 
++/* This uses a weird Auto prefix to avoid conflicts with later added polkit types. */
++typedef PolkitAuthorizationResult AutoPolkitAuthorizationResult;
++typedef PolkitSubject             AutoPolkitSubject;
++
++G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitAuthorizationResult, g_object_unref)
++G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitSubject, g_object_unref)
++
+ static const char *
+ get_active_profile (PpdApp *data)
+ {
+@@ -603,8 +610,8 @@ check_action_permission (PpdApp
+                          GError               **error)
+ {
+   g_autoptr(GError) local_error = NULL;
+-  g_autoptr(PolkitAuthorizationResult) result = NULL;
+-  g_autoptr(PolkitSubject) subject = NULL;
++  g_autoptr(AutoPolkitAuthorizationResult) result = NULL;
++  g_autoptr(AutoPolkitSubject) subject = NULL;
+ 
+   subject = polkit_system_bus_name_new (sender);
+   result = polkit_authority_check_authorization_sync (data->auth,
diff --git a/debian/patches/series b/debian/patches/series
index e69de29..1a4a0bc 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -0,0 +1 @@
+build_older_polkit.patch
diff --git a/docs/meson.build b/docs/meson.build
index cfd2406..489658b 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -24,6 +24,7 @@ private_headers = [
   'ppd-driver-balanced.h',
   'ppd-driver-fake.h',
   'ppd-driver-intel-pstate.h',
+  'ppd-driver-placeholder.h',
   'ppd-driver-platform-profile.h',
   'ppd-driver-power-saver.h',
   'ppd-utils.h',
diff --git a/meson.build b/meson.build
index 81181f6..675bb10 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('power-profiles-daemon', [ 'c' ],
-        version: '0.8.1',
+        version: '0.10.1',
         license: 'GPLv3+',
         default_options: [
           'buildtype=debugoptimized',
@@ -12,9 +12,6 @@ cc = meson.get_compiler('c')
 
 common_cflags = cc.get_supported_arguments([
     '-fgnu89-inline',
-    '-fvisibility=hidden',
-    '-std=gnu99',
-    '-Wall',
     '-Wundef',
     '-Wunused',
     '-Wstrict-prototypes',
@@ -37,13 +34,19 @@ endif
 gio_dep = dependency('gio-2.0')
 gudev_dep = dependency('gudev-1.0', version: '>= 234')
 upower_dep = dependency('upower-glib')
+polkit_gobject_dep = dependency('polkit-gobject-1', version: '>= 0.91')
+polkit_policy_directory = polkit_gobject_dep.get_pkgconfig_variable('policydir')
 
 gnome = import('gnome')
 
 add_global_arguments('-D_GNU_SOURCE=1', language: 'c')
+add_global_arguments(common_cflags, language: 'c')
 
-pylint = find_program('pylint-3', 'pylint3', 'pylint', required: false)
-pylint_flags = ['-d', 'C0116', '-d', 'C0114', '-d', 'W0707']
+if get_option('pylint')
+    pylint = find_program('pylint-3', 'pylint3', 'pylint', required: true)
+    pylint_flags = ['-d', 'C0116', '-d', 'C0114', '-d', 'W0707']
+endif
+xmllint = find_program('xmllint', required: false)
 
 subdir('src')
 subdir('data')
diff --git a/meson_options.txt b/meson_options.txt
index a3789f6..7e89619 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -6,3 +6,7 @@ option('gtk_doc',
        type: 'boolean',
        value: false,
        description: 'Build docs')
+option('pylint',
+       type: 'boolean',
+       value: false,
+       description: 'Run pylint checks, for developers only')
diff --git a/src/meson.build b/src/meson.build
index 318f284..f20e42a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,4 +1,11 @@
-deps = [ gio_dep, gudev_dep, upower_dep ]
+deps = [ gio_dep, gudev_dep, upower_dep, polkit_gobject_dep ]
+
+config_h = configuration_data()
+config_h.set_quoted('VERSION', meson.project_version())
+config_h_files = configure_file(
+  output: 'config.h',
+  configuration: config_h
+)
 
 resources = gnome.compile_resources(
     'power-profiles-daemon-resources', 'power-profiles-daemon.gresource.xml',
@@ -65,7 +72,7 @@ script = configure_file(
   install_dir: get_option('bindir')
 )
 
-if pylint.found()
+if get_option('pylint')
   test('pylint-powerprofilesctl',
        pylint,
        args: pylint_flags + [ script ],
diff --git a/src/net.hadess.PowerProfiles.xml b/src/net.hadess.PowerProfiles.xml
index fce04a8..fcfc1d4 100644
--- a/src/net.hadess.PowerProfiles.xml
+++ b/src/net.hadess.PowerProfiles.xml
@@ -13,7 +13,7 @@
 
       OS components would typically use the "Profiles" property to construct
       their UI (2 or 3 profiles available), and monitor the "ActiveProfile"
-      and the "PerformanceInhibited" properties to update that UI. The UI
+      and the "PerformanceDegraded" properties to update that UI. The UI
       would try to set the "ActiveProfile" property if the user selected
       a different one.
 
@@ -24,27 +24,78 @@
       The object path will be "/net/hadess/PowerProfiles".
   -->
   <interface name="net.hadess.PowerProfiles">
+
+    <!--
+        HoldProfile:
+
+        This forces the passed profile (either 'power-saver' or 'performance')
+        to be activated until either the caller quits, "ReleaseProfile" is
+        called, or the "ActiveProfile" is changed by the user.
+
+        This should be used programmatically by OS components when, eg. high-
+        performance workloads are started with the "performance" profile, or
+        battery will soon be critically low with the "power-saver" profile.
+
+        When conflicting profiles are requested to be held, the 'power-saver' profile
+        will be activated in preference to the 'performance' profile.
+
+        Those holds will be automatically cancelled if the user manually switches
+        to another profile, and the "ProfileReleased" signal will be emitted.
+    -->
+    <method name="HoldProfile">
+      <arg name="profile" type="s" direction="in"/>
+      <arg name="reason" type="s" direction="in"/>
+      <arg name="application_id" type="s" direction="in" />
+      <arg name="cookie" type="u" direction="out"/>
+    </method>
+
+    <!--
+        ReleaseProfile:
+
+        This removes the hold that was set on a profile.
+    -->
+    <method name="ReleaseProfile">
+      <arg name="cookie" type="u" direction="in"/>
+    </method>
+
+    <!--
+        ProfileReleased:
+
+        This signal will be emitted if the profile is released because the
+        "ActiveProfile" was manually changed. The signal will only be emitted
+        to the process that originally called "HoldProfile".
+    -->
+    <signal name="ProfileReleased">
+      <arg name="cookie" type="u" direction="out"/>
+    </signal>
+
     <!--
         ActiveProfile:
 
         The type of the currently active profile. It might change automatically
-        if the "performance" profile was selected but it got inhibited, in which
-        case the "PerformanceInhibited" property will reflect the reason.
+        if a profile is held, using the "HoldProfile" function.
     -->
     <property name="ActiveProfile" type="s" access="readwrite"/>
 
     <!--
         PerformanceInhibited:
 
-        This will be set if the performance power profile is unavailable, with
-        the value being used to identify the reason for unavailability. As new
-        reasons can be added, it is recommended that front-ends show a generic
+        This property is deprecated, and unused since version 0.9.
+    -->
+    <property name="PerformanceInhibited" type="s" access="read"/>
+
+    <!--
+        PerformanceDegraded:
+
+        This will be set if the performance power profile is running in degraded
+        mode, with the value being used to identify the reason for that degradation.
+        As new reasons can be added, it is recommended that front-ends show a generic
         reason if they do not recognise the value. Possible values are:
         - "lap-detected" (the computer is sitting on the user's lap)
         - "high-operating-temperature" (the computer is close to overheating)
-        - "" (the empty string, if not inhibited)
+        - "" (the empty string, if not performance is not degraded)
     -->
-    <property name="PerformanceInhibited" type="s" access="read"/>
+    <property name="PerformanceDegraded" type="s" access="read"/>
 
     <!--
         Profiles:
@@ -60,6 +111,9 @@
 
         Only one of each type of profile will be listed, with the daemon choosing the
         more appropriate "driver" for each profile type.
+
+        This list is guaranteed to be sorted in the same order that the profiles
+        are listed above.
     -->
     <property name="Profiles" type="aa{sv}" access="read"/>
 
@@ -72,5 +126,15 @@
     -->
     <property name="Actions" type="as" access="read"/>
 
+    <!--
+      ActiveProfileHolds:
+
+      A list of dictionaries representing the current profile holds.
+      The keys in the dict are "ApplicationId", "Profile" and "Reason",
+      and correspond to the "application_id", "profile" and "reason" arguments
+      passed to the HoldProfile() method.
+    -->
+    <property name="ActiveProfileHolds" type="aa{sv}" access="read"/>
+
   </interface>
 </node>
diff --git a/src/power-profiles-daemon.c b/src/power-profiles-daemon.c
index c07d8a5..7128ac8 100644
--- a/src/power-profiles-daemon.c
+++ b/src/power-profiles-daemon.c
@@ -1,5 +1,6 @@
 /*
- * Copyright (c) 2014-2016, 2020 Bastien Nocera <hadess at hadess.net>
+ * Copyright (c) 2014-2016, 2020-2021 Bastien Nocera <hadess at hadess.net>
+ * Copyright (c) 2021 David Redondo <kde at david-redondo.de>
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 3 as published by
@@ -7,7 +8,10 @@
  *
  */
 
+#include "config.h"
+
 #include <locale.h>
+#include <polkit/polkit.h>
 
 #include "power-profiles-daemon-resources.h"
 #include "power-profiles-daemon.h"
@@ -27,12 +31,37 @@ typedef struct {
   gboolean was_started;
   int ret;
 
+  GKeyFile *config;
+  char *config_path;
+
+  PolkitAuthority *auth;
+
   PpdProfile active_profile;
+  PpdProfile selected_profile;
   GPtrArray *probed_drivers;
   PpdDriver *driver;
   GPtrArray *actions;
+  GHashTable *profile_holds;
 } PpdApp;
 
+typedef struct {
+  PpdProfile profile;
+  char *reason;
+  char *application_id;
+  char *requester;
+} ProfileHold;
+
+static void
+profile_hold_free (ProfileHold *hold)
+{
+  if (hold == NULL)
+    return;
+  g_free (hold->reason);
+  g_free (hold->application_id);
+  g_free (hold->requester);
+  g_free (hold);
+}
+
 static PpdApp *ppd_app = NULL;
 
 static void stop_profile_drivers (PpdApp *data);
@@ -68,9 +97,11 @@ typedef enum {
   PROP_INHIBITED                  = 1 << 1,
   PROP_PROFILES                   = 1 << 2,
   PROP_ACTIONS                    = 1 << 3,
+  PROP_DEGRADED                   = 1 << 4,
+  PROP_ACTIVE_PROFILE_HOLDS       = 1 << 5
 } PropertiesMask;
 
-#define PROP_ALL (PROP_ACTIVE_PROFILE | PROP_INHIBITED | PROP_PROFILES | PROP_ACTIONS)
+#define PROP_ALL (PROP_ACTIVE_PROFILE | PROP_INHIBITED | PROP_PROFILES | PROP_ACTIONS | PROP_DEGRADED | PROP_ACTIVE_PROFILE_HOLDS)
 
 static const char *
 get_active_profile (PpdApp *data)
@@ -79,7 +110,7 @@ get_active_profile (PpdApp *data)
 }
 
 static const char *
-get_performance_inhibited (PpdApp *data)
+get_performance_degraded (PpdApp *data)
 {
   const char *ret;
   PpdDriver *driver;
@@ -87,7 +118,7 @@ get_performance_inhibited (PpdApp *data)
   driver = GET_DRIVER(PPD_PROFILE_PERFORMANCE);
   if (!driver)
     return "";
-  ret = ppd_driver_get_performance_inhibited (driver);
+  ret = ppd_driver_get_performance_degraded (driver);
   g_assert (ret != NULL);
   return ret;
 }
@@ -136,6 +167,33 @@ get_actions_variant (PpdApp *data)
   return g_variant_builder_end (&builder);
 }
 
+static GVariant *
+get_profile_holds_variant (PpdApp *data)
+{
+  GVariantBuilder builder;
+  GHashTableIter iter;
+  gpointer value;
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
+  g_hash_table_iter_init (&iter, data->profile_holds);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value)) {
+    GVariantBuilder asv_builder;
+    ProfileHold *hold = value;
+
+    g_variant_builder_init (&asv_builder, G_VARIANT_TYPE ("a{sv}"));
+    g_variant_builder_add (&asv_builder, "{sv}", "ApplicationId",
+                           g_variant_new_string (hold->application_id));
+    g_variant_builder_add (&asv_builder, "{sv}", "Profile",
+                           g_variant_new_string (ppd_profile_to_str (hold->profile)));
+    g_variant_builder_add (&asv_builder, "{sv}", "Reason", g_variant_new_string (hold->reason));
+
+    g_variant_builder_add (&builder, "a{sv}", &asv_builder);
+  }
+
+  return g_variant_builder_end (&builder);
+}
+
 static void
 send_dbus_event (PpdApp     *data,
                  PropertiesMask  mask)
@@ -158,7 +216,11 @@ send_dbus_event (PpdApp     *data,
   }
   if (mask & PROP_INHIBITED) {
     g_variant_builder_add (&props_builder, "{sv}", "PerformanceInhibited",
-                           g_variant_new_string (get_performance_inhibited (data)));
+                           g_variant_new_string (""));
+  }
+  if (mask & PROP_DEGRADED) {
+    g_variant_builder_add (&props_builder, "{sv}", "PerformanceDegraded",
+                           g_variant_new_string (get_performance_degraded (data)));
   }
   if (mask & PROP_PROFILES) {
     g_variant_builder_add (&props_builder, "{sv}", "Profiles",
@@ -168,6 +230,10 @@ send_dbus_event (PpdApp     *data,
     g_variant_builder_add (&props_builder, "{sv}", "Actions",
                            get_actions_variant (data));
   }
+  if (mask & PROP_ACTIVE_PROFILE_HOLDS) {
+    g_variant_builder_add (&props_builder, "{sv}", "ActiveProfileHolds",
+                           get_profile_holds_variant (data));
+  }
 
   props_changed = g_variant_new ("(s at a{sv}@as)", POWER_PROFILES_IFACE_NAME,
                                  g_variant_builder_end (&props_builder),
@@ -181,6 +247,56 @@ send_dbus_event (PpdApp     *data,
                                  props_changed, NULL);
 }
 
+static void
+save_configuration (PpdApp *data)
+{
+  g_autoptr(GError) error = NULL;
+
+  g_key_file_set_string (data->config, "State", "Driver", ppd_driver_get_driver_name (data->driver));
+  g_key_file_set_string (data->config, "State", "Profile", ppd_profile_to_str (data->active_profile));
+  if (!g_key_file_save_to_file (data->config, data->config_path, &error))
+    g_warning ("Could not save configuration file '%s': %s", data->config_path, error->message);
+}
+
+static gboolean
+apply_configuration (PpdApp *data)
+{
+  g_autofree char *driver = NULL;
+  g_autofree char *profile_str = NULL;
+  PpdProfile profile;
+
+  driver = g_key_file_get_string (data->config, "State", "Driver", NULL);
+  if (g_strcmp0 (ppd_driver_get_driver_name (data->driver), driver) != 0)
+    return FALSE;
+  profile_str = g_key_file_get_string (data->config, "State", "Profile", NULL);
+  if (profile_str == NULL)
+    return FALSE;
+  profile = ppd_profile_from_str (profile_str);
+  if (profile == PPD_PROFILE_UNSET) {
+    g_debug ("Resetting invalid configuration profile '%s'", profile_str);
+    g_key_file_remove_key (data->config, "State", "Profile", NULL);
+    return FALSE;
+  }
+
+  g_debug ("Applying profile '%s' from configuration file", profile_str);
+  data->active_profile = profile;
+  return TRUE;
+}
+
+static void
+load_configuration (PpdApp *data)
+{
+  g_autoptr(GError) error = NULL;
+
+  if (g_getenv ("UMOCKDEV_DIR") != NULL)
+    data->config_path = g_build_filename (g_getenv ("UMOCKDEV_DIR"), "ppd_test_conf.ini", NULL);
+  else
+    data->config_path = g_strdup ("/var/lib/power-profiles-daemon/state.ini");
+  data->config = g_key_file_new ();
+  if (!g_key_file_load_from_file (data->config, data->config_path, G_KEY_FILE_KEEP_COMMENTS, &error))
+    g_debug ("Could not load configuration file '%s': %s", data->config_path, error->message);
+}
+
 static void
 actions_activate_profile (GPtrArray *actions,
                           PpdProfile profile)
@@ -226,6 +342,29 @@ activate_target_profile (PpdApp                     *data,
   actions_activate_profile (data->actions, target_profile);
 
   data->active_profile = target_profile;
+
+  if (reason == PPD_PROFILE_ACTIVATION_REASON_USER ||
+      reason == PPD_PROFILE_ACTIVATION_REASON_INTERNAL)
+    save_configuration (data);
+}
+
+static void
+release_all_profile_holds (PpdApp *data)
+{
+  GHashTableIter iter;
+  gpointer key, value;
+
+  g_hash_table_iter_init (&iter, data->profile_holds);
+  while (g_hash_table_iter_next (&iter, &key, &value)) {
+    ProfileHold *hold = value;
+    guint cookie = GPOINTER_TO_UINT (key);
+
+    g_dbus_connection_emit_signal (data->connection, hold->requester, POWER_PROFILES_DBUS_PATH,
+                                   POWER_PROFILES_IFACE_NAME, "ProfileReleased",
+                                   g_variant_new ("(u)", cookie), NULL);
+    g_bus_unwatch_name (cookie);
+  }
+  g_hash_table_remove_all (data->profile_holds);
 }
 
 static gboolean
@@ -234,6 +373,7 @@ set_active_profile (PpdApp      *data,
                     GError     **error)
 {
   PpdProfile target_profile;
+  guint mask = PROP_ACTIVE_PROFILE;
 
   target_profile = ppd_profile_from_str (profile);
   if (target_profile == PPD_PROFILE_UNSET) {
@@ -245,49 +385,64 @@ set_active_profile (PpdApp      *data,
   if (target_profile == data->active_profile)
     return TRUE;
 
-  if (target_profile == PPD_PROFILE_PERFORMANCE &&
-      ppd_driver_is_performance_inhibited (GET_DRIVER (PPD_PROFILE_PERFORMANCE))) {
-    g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
-                 "Profile '%s' is inhibited", profile);
-    return FALSE;
-  }
-
   g_debug ("Transitioning active profile from '%s' to '%s' by user request",
            ppd_profile_to_str (data->active_profile), profile);
 
+  if (g_hash_table_size (data->profile_holds) != 0 ) {
+    g_debug ("Releasing active profile holds");
+    release_all_profile_holds (data);
+    mask |= PROP_ACTIVE_PROFILE_HOLDS;
+  }
+
   activate_target_profile (data, target_profile, PPD_PROFILE_ACTIVATION_REASON_USER);
-  send_dbus_event (data, PROP_ACTIVE_PROFILE);
+  data->selected_profile = target_profile;
+  send_dbus_event (data, mask);
 
   return TRUE;
 }
 
+static PpdProfile
+effective_hold_profile (PpdApp *data)
+{
+  GHashTableIter iter;
+  gpointer value;
+  PpdProfile profile = PPD_PROFILE_UNSET;
+
+  g_hash_table_iter_init (&iter, data->profile_holds);
+  while (g_hash_table_iter_next (&iter, NULL, &value)) {
+    ProfileHold *hold = value;
+
+    if (hold->profile == PPD_PROFILE_POWER_SAVER) {
+      profile = PPD_PROFILE_POWER_SAVER;
+      break;
+    }
+    profile = hold->profile;
+  }
+  return profile;
+}
+
 static void
-driver_performance_inhibited_changed_cb (GObject    *gobject,
-                                         GParamSpec *pspec,
-                                         gpointer    user_data)
+driver_performance_degraded_changed_cb (GObject    *gobject,
+                                        GParamSpec *pspec,
+                                        gpointer    user_data)
 {
   PpdApp *data = user_data;
   PpdDriver *driver = PPD_DRIVER (gobject);
   const char *prop_str = pspec->name;
 
-  if (g_strcmp0 (prop_str, "performance-inhibited") != 0) {
+  if (g_strcmp0 (prop_str, "performance-degraded") != 0) {
     g_warning ("Ignoring '%s' property change on profile driver '%s'",
                prop_str, ppd_driver_get_driver_name (driver));
     return;
   }
 
   if (!(ppd_driver_get_profiles (driver) & PPD_PROFILE_PERFORMANCE)) {
-    g_warning ("Ignored 'performance-inhibited' change on non-performance driver '%s'",
+    g_warning ("Ignored 'performance-degraded' change on non-performance driver '%s'",
                ppd_driver_get_driver_name (driver));
     return;
   }
 
-  send_dbus_event (data, PROP_INHIBITED);
-  if (!ppd_driver_is_performance_inhibited (driver))
-    return;
-
-  activate_target_profile (data, PPD_PROFILE_BALANCED, PPD_PROFILE_ACTIVATION_REASON_INHIBITION);
-  send_dbus_event (data, PROP_ACTIVE_PROFILE);
+  send_dbus_event (data, PROP_DEGRADED);
 }
 
 static void
@@ -308,6 +463,169 @@ driver_profile_changed_cb (PpdDriver *driver,
   send_dbus_event (data, PROP_ACTIVE_PROFILE);
 }
 
+static void
+release_profile_hold (PpdApp *data,
+                      guint   cookie)
+{
+  guint mask = PROP_ACTIVE_PROFILE_HOLDS;
+  ProfileHold *hold;
+  PpdProfile hold_profile, next_profile;
+
+  hold = g_hash_table_lookup (data->profile_holds, GUINT_TO_POINTER (cookie));
+  if (!hold) {
+    g_debug("No hold with cookie %d", cookie);
+    return;
+  }
+
+  g_bus_unwatch_name (cookie);
+  hold_profile = hold->profile;
+  g_hash_table_remove (data->profile_holds, GUINT_TO_POINTER (cookie));
+
+  if (g_hash_table_size (data->profile_holds) == 0 &&
+      hold_profile != data->selected_profile) {
+    g_debug ("No profile holds anymore going back to last manually activated profile");
+    activate_target_profile (data, data->selected_profile, PPD_PROFILE_ACTIVATION_REASON_PROGRAM_HOLD);
+    mask |= PROP_ACTIVE_PROFILE;
+  } else if (hold_profile == data->active_profile) {
+    next_profile = effective_hold_profile (data);
+    if (next_profile != PPD_PROFILE_UNSET &&
+        next_profile != data->active_profile) {
+      g_debug ("Next profile is %s", ppd_profile_to_str (next_profile));
+      activate_target_profile (data, next_profile, PPD_PROFILE_ACTIVATION_REASON_PROGRAM_HOLD);
+      mask |= PROP_ACTIVE_PROFILE;
+    }
+  }
+
+  send_dbus_event (data, mask);
+}
+
+static void
+holder_disappeared (GDBusConnection *connection,
+                    const gchar     *name,
+                    gpointer         user_data)
+{
+  PpdApp *data = user_data;
+  GHashTableIter iter;
+  gpointer key, value;
+  GPtrArray *cookies;
+  guint i;
+
+  cookies = g_ptr_array_new ();
+  g_hash_table_iter_init (&iter, data->profile_holds);
+  while (g_hash_table_iter_next (&iter, &key, (gpointer *) &value)) {
+    guint cookie = GPOINTER_TO_UINT (key);
+    ProfileHold *hold = value;
+
+    if (g_strcmp0 (hold->requester, name) != 0)
+      continue;
+
+    g_debug ("Holder %s with cookie %u disappeared, adding to list", name, cookie);
+    g_ptr_array_add (cookies, GUINT_TO_POINTER (cookie));
+  }
+
+  for (i = 0; i < cookies->len; i++) {
+    guint cookie = GPOINTER_TO_UINT (cookies->pdata[i]);
+    g_debug ("Removing profile hold for cookie %u", cookie);
+    release_profile_hold (data, cookie);
+  }
+  g_ptr_array_free (cookies, TRUE);
+}
+
+static void
+hold_profile (PpdApp                *data,
+              GVariant              *parameters,
+              GDBusMethodInvocation *invocation)
+{
+  const char *profile_name;
+  const char *reason;
+  const char *application_id;
+  PpdProfile profile;
+  ProfileHold *hold;
+  guint watch_id;
+  guint mask;
+
+  g_variant_get (parameters, "(&s&s&s)", &profile_name, &reason, &application_id);
+  profile = ppd_profile_from_str (profile_name);
+  if (profile != PPD_PROFILE_PERFORMANCE &&
+      profile != PPD_PROFILE_POWER_SAVER) {
+    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+                                           "Only profiles 'performance' and 'power-saver' can be a hold profile");
+    return;
+  }
+
+  hold = g_new0 (ProfileHold, 1);
+  hold->profile = profile;
+  hold->reason = g_strdup (reason);
+  hold->application_id = g_strdup (application_id);
+  hold->requester = g_strdup (g_dbus_method_invocation_get_sender (invocation));
+
+  g_debug ("%s(%s) requesting to hold profile '%s', reason: '%s'", application_id,
+           hold->requester, profile_name, reason);
+  watch_id = g_bus_watch_name_on_connection (data->connection, hold->requester,
+                                             G_BUS_NAME_WATCHER_FLAGS_NONE, NULL,
+                                             holder_disappeared, data, NULL);
+  g_hash_table_insert (data->profile_holds, GUINT_TO_POINTER (watch_id), hold);
+  g_dbus_method_invocation_return_value (invocation, g_variant_new ("(u)", watch_id));
+  mask = PROP_ACTIVE_PROFILE_HOLDS;
+
+  if (profile != data->active_profile) {
+    PpdProfile target_profile = effective_hold_profile (data);
+    if (target_profile != PPD_PROFILE_UNSET &&
+        target_profile != data->active_profile) {
+      activate_target_profile (data, target_profile, PPD_PROFILE_ACTIVATION_REASON_PROGRAM_HOLD);
+      mask |= PROP_ACTIVE_PROFILE;
+    }
+  }
+
+  send_dbus_event (data, mask);
+}
+
+static void
+release_profile (PpdApp                *data,
+                 GVariant              *parameters,
+                 GDBusMethodInvocation *invocation)
+{
+  guint cookie;
+  g_variant_get (parameters, "(u)", &cookie);
+  if (!g_hash_table_contains (data->profile_holds, GUINT_TO_POINTER (cookie))) {
+    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+                                           "No hold with cookie  %d", cookie);
+    return;
+  }
+  release_profile_hold (data, cookie);
+  g_dbus_method_invocation_return_value (invocation, NULL);
+}
+
+static gboolean
+check_action_permission (PpdApp                *data,
+                         const char            *sender,
+                         const char            *action,
+                         GError               **error)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autoptr(PolkitAuthorizationResult) result = NULL;
+  g_autoptr(PolkitSubject) subject = NULL;
+
+  subject = polkit_system_bus_name_new (sender);
+  result = polkit_authority_check_authorization_sync (data->auth,
+                                                      subject,
+                                                      action,
+                                                      NULL,
+                                                      POLKIT_CHECK_AUTHORIZATION_FLAGS_NONE,
+                                                      NULL, &local_error);
+  if (result == NULL ||
+      !polkit_authorization_result_get_is_authorized (result))
+    {
+      g_set_error (error, G_DBUS_ERROR,
+                   G_DBUS_ERROR_ACCESS_DENIED,
+                   "Not Authorized: %s", local_error ? local_error->message : action);
+      return FALSE;
+    }
+
+  return TRUE;
+
+}
+
 static GVariant *
 handle_get_property (GDBusConnection *connection,
                      const gchar     *sender,
@@ -324,11 +642,15 @@ handle_get_property (GDBusConnection *connection,
   if (g_strcmp0 (property_name, "ActiveProfile") == 0)
     return g_variant_new_string (get_active_profile (data));
   if (g_strcmp0 (property_name, "PerformanceInhibited") == 0)
-    return g_variant_new_string (get_performance_inhibited (data));
+    return g_variant_new_string ("");
   if (g_strcmp0 (property_name, "Profiles") == 0)
     return get_profiles_variant (data);
   if (g_strcmp0 (property_name, "Actions") == 0)
     return get_actions_variant (data);
+  if (g_strcmp0 (property_name, "PerformanceDegraded") == 0)
+    return g_variant_new_string (get_performance_degraded (data));
+  if (g_strcmp0 (property_name, "ActiveProfileHolds") == 0)
+    return get_profile_holds_variant (data);
   return NULL;
 }
 
@@ -352,14 +674,55 @@ handle_set_property (GDBusConnection  *connection,
                  "No such property: %s", property_name);
     return FALSE;
   }
+  if (!check_action_permission (data, sender, "net.hadess.PowerProfiles.switch-profile", error))
+    return FALSE;
 
   g_variant_get (value, "&s", &profile);
   return set_active_profile (data, profile, error);
 }
 
+static void
+handle_method_call (GDBusConnection       *connection,
+                    const gchar           *sender,
+                    const gchar           *object_path,
+                    const gchar           *interface_name,
+                    const gchar           *method_name,
+                    GVariant              *parameters,
+                    GDBusMethodInvocation *invocation,
+                    gpointer               user_data)
+{
+  PpdApp *data = user_data;
+  g_assert (data->connection);
+
+  if (g_strcmp0 (interface_name, POWER_PROFILES_IFACE_NAME) != 0) {
+    g_dbus_method_invocation_return_error (invocation,G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE,
+                                           "Unknown interface %s", interface_name);
+    return;
+  }
+
+  if (g_strcmp0 (method_name, "HoldProfile") == 0) {
+    g_autoptr(GError) local_error = NULL;
+    if (!check_action_permission (data,
+                                  g_dbus_method_invocation_get_sender (invocation),
+                                  "net.hadess.PowerProfiles.hold-profile",
+                                  &local_error)) {
+      g_dbus_method_invocation_return_gerror (invocation, local_error);
+      return;
+    }
+    hold_profile (data, parameters, invocation);
+  } else if (g_strcmp0 (method_name, "ReleaseProfile") == 0) {
+    release_profile (data, parameters, invocation);
+  } else {
+      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD,
+                                             "No such method %s in interface %s", interface_name,
+                                              method_name);
+  }
+}
+
+
 static const GDBusInterfaceVTable interface_vtable =
 {
-  NULL,
+  handle_method_call,
   handle_get_property,
   handle_set_property
 };
@@ -422,6 +785,7 @@ driver_probe_request_cb (PpdDriver *driver,
 static void
 stop_profile_drivers (PpdApp *data)
 {
+  release_all_profile_holds (data);
   g_ptr_array_set_size (data->probed_drivers, 0);
   g_ptr_array_set_size (data->actions, 0);
   g_clear_object (&data->driver);
@@ -431,9 +795,6 @@ static void
 start_profile_drivers (PpdApp *data)
 {
   guint i;
-  PpdProfile prev_profile;
-
-  prev_profile = data->active_profile;
 
   for (i = 0; i < G_N_ELEMENTS (objects); i++) {
     GObject *object;
@@ -462,7 +823,7 @@ start_profile_drivers (PpdApp *data)
         continue;
       }
 
-      result = ppd_driver_probe (driver, &prev_profile);
+      result = ppd_driver_probe (driver);
       if (result == PPD_PROBE_RESULT_FAIL) {
         g_debug ("probe() failed for driver %s, skipping",
                  ppd_driver_get_driver_name (driver));
@@ -477,8 +838,8 @@ start_profile_drivers (PpdApp *data)
 
       data->driver = driver;
 
-      g_signal_connect (G_OBJECT (driver), "notify::performance-inhibited",
-                        G_CALLBACK (driver_performance_inhibited_changed_cb), data);
+      g_signal_connect (G_OBJECT (driver), "notify::performance-degraded",
+                        G_CALLBACK (driver_performance_degraded_changed_cb), data);
       g_signal_connect (G_OBJECT (driver), "profile-changed",
                         G_CALLBACK (driver_profile_changed_cb), data);
     } else if (PPD_IS_ACTION (object)) {
@@ -504,13 +865,8 @@ start_profile_drivers (PpdApp *data)
     goto bail;
   }
 
-  if (prev_profile != data->active_profile) {
-    g_debug ("Using '%s' as current profile from probed driver",
-             ppd_profile_to_str (prev_profile));
-    data->active_profile = prev_profile;
-  }
-
-  /* Set initial state */
+  /* Set initial state either from configuration, or using the currently selected profile */
+  apply_configuration (data);
   activate_target_profile (data, data->active_profile, PPD_PROFILE_ACTIVATION_REASON_RESET);
 
   send_dbus_event (data, PROP_ALL);
@@ -583,9 +939,14 @@ free_app_data (PpdApp *data)
     data->name_id = 0;
   }
 
+  g_clear_pointer (&data->config_path, g_free);
+  g_clear_pointer (&data->config, g_key_file_unref);
   g_ptr_array_free (data->probed_drivers, TRUE);
   g_ptr_array_free (data->actions, TRUE);
   g_clear_object (&data->driver);
+  g_hash_table_destroy (data->profile_holds);
+
+  g_clear_object (&data->auth);
 
   g_clear_pointer (&data->main_loop, g_main_loop_unref);
   g_clear_pointer (&data->introspection_data, g_dbus_node_info_unref);
@@ -627,11 +988,17 @@ int main (int argc, char **argv)
   if (verbose)
     g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
 
+  g_debug ("Starting power-profiles-daemon version "VERSION);
+
   data = g_new0 (PpdApp, 1);
   data->main_loop = g_main_loop_new (NULL, TRUE);
+  data->auth = polkit_authority_get_sync (NULL, NULL);
   data->probed_drivers = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
   data->actions = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+  data->profile_holds = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) profile_hold_free);
   data->active_profile = PPD_PROFILE_BALANCED;
+  data->selected_profile = PPD_PROFILE_BALANCED;
+  load_configuration (data);
   ppd_app = data;
 
   /* Set up D-Bus */
diff --git a/src/powerprofilesctl.in b/src/powerprofilesctl.in
index b525e91..9b5e201 100755
--- a/src/powerprofilesctl.in
+++ b/src/powerprofilesctl.in
@@ -1,5 +1,7 @@
 #!@PYTHON3@
 
+import signal
+import subprocess
 import sys
 from gi.repository import Gio, GLib
 
@@ -15,6 +17,8 @@ def usage_main():
     print('  get        Print the currently active power profile')
     print('  set        Set the currently active power profile')
     print('  list       List available power profiles')
+    print('  list-holds List current power profile holds')
+    print('  launch     Launch a command while holding a power profile')
     print('')
     print('Use “powerprofilesctl help COMMAND” to get detailed help.')
 
@@ -43,6 +47,28 @@ def usage_list():
     print('')
     print('List available power profiles.')
 
+def usage_list_holds():
+    print('Usage:')
+    print('  powerprofilesctl list-holds')
+    print('')
+    print('List current power profile holds.')
+
+def usage_launch():
+    print('Usage:')
+    print('  powerprofilesctl launch [COMMAND…]')
+    print('')
+    print('Launch a command while holding a power profile.')
+    print('')
+    print('Options:')
+    print('  -p, --profile=PROFILE           The power profile to hold')
+    print('  -r, --reason=REASON             The reason for the profile hold')
+    print('  -i, --appid=APP-ID              The application ID for the profile hold')
+    print('')
+    print('Launch the command while holding a power profile, either performance, ')
+    print('or power-saver. By default, the profile hold is for the performance ')
+    print('profile, but it might not be available on all systems. See the list ')
+    print('command for a list of available profiles.')
+
 def usage(_command=None):
     if not _command:
         usage_main()
@@ -52,6 +78,10 @@ def usage(_command=None):
         usage_set()
     elif _command == 'list':
         usage_list()
+    elif _command == 'list-holds':
+        usage_list_holds()
+    elif _command == 'launch':
+        usage_launch()
     elif _command == 'version':
         usage_version()
     else:
@@ -100,8 +130,8 @@ def get_profiles_property(prop):
 def _list():
     try:
         profiles = get_profiles_property('Profiles')
-        reason = get_proxy().Get('(ss)', 'net.hadess.PowerProfiles', 'PerformanceInhibited')
-        inhibited = (reason != '')
+        reason = get_proxy().Get('(ss)', 'net.hadess.PowerProfiles', 'PerformanceDegraded')
+        degraded = (reason != '')
         active = get_proxy().Get('(ss)', 'net.hadess.PowerProfiles', 'ActiveProfile')
     except:
         print("Couldn\'t get Profiles: ", sys.exc_info()[0])
@@ -111,13 +141,53 @@ def _list():
         for profile in reversed(profiles):
             if index > 0:
                 print('')
-            print(('%s %s:') % ('*' if profile['Profile'] == active else ' ', profile['Profile']))
+            marker = '*' if profile['Profile'] == active else ' '
+            print(f'{marker} {profile["Profile"]}:')
             print('    Driver:    ', profile['Driver'])
             if profile['Profile'] == 'performance':
-                print('    Inhibited: ', f'yes ({reason})' if inhibited else 'no')
+                print('    Degraded:  ', f'yes ({reason})' if degraded else 'no')
             index += 1
 
-def main(): # pylint: disable=too-many-branches
+def _list_holds():
+    try:
+        holds = get_profiles_property('ActiveProfileHolds')
+    except:
+        # print("Couldn\'t get ActiveProfileHolds: ", sys.exc_info()[0])
+        raise SystemError
+    else:
+        index = 0
+        for hold in holds:
+            if index > 0:
+                print('')
+            print('Hold:')
+            print('  Profile:        ', hold['Profile'])
+            print('  Application ID: ', hold['ApplicationId'])
+            print('  Reason:         ', hold['Reason'])
+            index += 1
+
+def _launch(args, profile, appid, reason):
+    try:
+        bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
+        proxy = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None,
+                                       'net.hadess.PowerProfiles',
+                                       '/net/hadess/PowerProfiles',
+                                       'net.hadess.PowerProfiles', None)
+    except:
+        raise SystemError
+
+    cookie = proxy.HoldProfile('(sss)', profile, reason, appid)
+
+    # Kill child when we go away
+    def receive_signal(_signum, _stack):
+        launched_app.terminate()
+    signal.signal(signal.SIGTERM, receive_signal)
+
+    # print (f'Got {cookie} for {profile} hold')
+    with subprocess.Popen(args) as launched_app:
+        launched_app.wait()
+    proxy.ReleaseProfile('(u)', cookie)
+
+def main(): # pylint: disable=too-many-branches, disable=too-many-statements
     args = None
     if len(sys.argv) == 1:
         command = 'list'
@@ -146,6 +216,47 @@ def main(): # pylint: disable=too-many-branches
         _set(args[0])
     elif command == 'list':
         _list()
+    elif command == 'list-holds':
+        _list_holds()
+    elif command == 'launch':
+        if len(args) == 0:
+            sys.exit(0)
+        profile = None
+        reason = None
+        appid = None
+        while True:
+            if args[0] == '--':
+                args = args[1:]
+                break
+            if args[0][:9] == '--profile' or args[0] == '-p':
+                if args[0][:10] == '--profile=':
+                    args = args[0].split('=') + args[1:]
+                profile = args[1]
+                args = args[2:]
+                continue
+            if args[0][:8] == '--reason' or args[0] == '-r':
+                if args[0][:9] == '--reason=':
+                    args = args[0].split('=') + args[1:]
+                reason = args[1]
+                args = args[2:]
+                continue
+            if args[0][:7] == '--appid' or args[0] == '-i':
+                if args[0][:8] == '--appid=':
+                    args = args[0].split('=') + args[1:]
+                appid = args[1]
+                args = args[2:]
+                continue
+            break
+
+        if len(args) < 1:
+            sys.exit(0)
+        if not appid:
+            appid = args[0]
+        if not reason:
+            reason = 'Running ' + appid
+        if not profile:
+            profile = 'performance'
+        _launch(args, profile, appid, reason)
 
 if __name__ == '__main__':
     main()
diff --git a/src/ppd-driver-fake.c b/src/ppd-driver-fake.c
index 431ba64..ecd6154 100644
--- a/src/ppd-driver-fake.c
+++ b/src/ppd-driver-fake.c
@@ -22,7 +22,9 @@ struct _PpdDriverFake
 
   gboolean tio_set;
   struct termios old_tio;
-  gboolean inhibited;
+  GIOChannel *channel;
+  guint watch_id;
+  gboolean degraded;
 };
 
 G_DEFINE_TYPE (PpdDriverFake, ppd_driver_fake, PPD_TYPE_DRIVER)
@@ -46,19 +48,19 @@ ppd_driver_fake_constructor (GType                  type,
 }
 
 static void
-toggle_inhibition (PpdDriverFake *fake)
+toggle_degradation (PpdDriverFake *fake)
 {
-  fake->inhibited = !fake->inhibited;
+  fake->degraded = !fake->degraded;
 
   g_object_set (G_OBJECT (fake),
-                "performance-inhibited", fake->inhibited ? "lap-detected" : NULL,
+                "performance-degraded", fake->degraded ? "lap-detected" : NULL,
                 NULL);
 }
 
 static void
 keyboard_usage (void)
 {
-  g_print ("Valid keys are: i (toggle inhibition), r (restart drivers), q/x (quit)\n");
+  g_print ("Valid keys are: d (toggle degradation), r (restart drivers), q/x (quit)\n");
 }
 
 static gboolean
@@ -80,9 +82,9 @@ check_keyboard (GIOChannel    *source,
     return TRUE;
 
   switch (buf[0]) {
-  case 'i':
-    g_print ("Toggling inhibition\n");
-    toggle_inhibition (fake);
+  case 'd':
+    g_print ("Toggling degradation\n");
+    toggle_degradation (fake);
     break;
   case 'r':
     g_print ("Restarting profile drivers\n");
@@ -103,7 +105,6 @@ check_keyboard (GIOChannel    *source,
 static gboolean
 setup_keyboard (PpdDriverFake *fake)
 {
-  GIOChannel *channel;
   struct termios new_tio;
 
   tcgetattr(STDIN_FILENO, &fake->old_tio);
@@ -111,18 +112,18 @@ setup_keyboard (PpdDriverFake *fake)
   new_tio.c_lflag &=(~ICANON & ~ECHO);
   tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
 
-  channel = g_io_channel_unix_new (STDIN_FILENO);
-  if (!channel) {
+  fake->channel = g_io_channel_unix_new (STDIN_FILENO);
+  if (!fake->channel) {
     g_warning ("Failed to open stdin");
     return FALSE;
   }
 
-  if (g_io_channel_set_encoding (channel, NULL, NULL) != G_IO_STATUS_NORMAL) {
+  if (g_io_channel_set_encoding (fake->channel, NULL, NULL) != G_IO_STATUS_NORMAL) {
     g_warning ("Failed to set stdin encoding to NULL");
     return FALSE;
   }
 
-  g_io_add_watch (channel, G_IO_IN, (GIOFunc) check_keyboard, fake);
+  fake->watch_id = g_io_add_watch (fake->channel, G_IO_IN, (GIOFunc) check_keyboard, fake);
   fake->tio_set = TRUE;
   return TRUE;
 }
@@ -142,8 +143,7 @@ envvar_set (const char *key)
 }
 
 static PpdProbeResult
-ppd_driver_fake_probe (PpdDriver  *driver,
-                       PpdProfile *prev_profile)
+ppd_driver_fake_probe (PpdDriver  *driver)
 {
   PpdDriverFake *fake;
 
@@ -177,6 +177,8 @@ ppd_driver_fake_finalize (GObject *object)
   PpdDriverFake *fake;
 
   fake = PPD_DRIVER_FAKE (object);
+  g_clear_pointer (&fake->channel, g_io_channel_unref);
+  g_clear_handle_id (&fake->watch_id, g_source_remove);
   if (fake->tio_set)
     tcsetattr(STDIN_FILENO, TCSANOW, &fake->old_tio);
   G_OBJECT_CLASS (ppd_driver_fake_parent_class)->finalize (object);
diff --git a/src/ppd-driver-intel-pstate.c b/src/ppd-driver-intel-pstate.c
index de84339..5725516 100644
--- a/src/ppd-driver-intel-pstate.c
+++ b/src/ppd-driver-intel-pstate.c
@@ -62,7 +62,7 @@ update_no_turbo (PpdDriverIntelPstate *pstate)
       turbo_disabled = TRUE;
   }
 
-  g_object_set (G_OBJECT (pstate), "performance-inhibited",
+  g_object_set (G_OBJECT (pstate), "performance-degraded",
                 turbo_disabled ? "high-operating-temperature" : NULL,
                 NULL);
 }
@@ -107,8 +107,7 @@ open_policy_dir (void)
 }
 
 static gboolean
-ppd_driver_intel_pstate_probe (PpdDriver  *driver,
-                               PpdProfile *prev_profile)
+ppd_driver_intel_pstate_probe (PpdDriver  *driver)
 {
   PpdDriverIntelPstate *pstate = PPD_DRIVER_INTEL_PSTATE (driver);
   g_autoptr(GDir) dir = NULL;
diff --git a/src/ppd-driver-platform-profile.c b/src/ppd-driver-platform-profile.c
index 9e20ee8..8111251 100644
--- a/src/ppd-driver-platform-profile.c
+++ b/src/ppd-driver-platform-profile.c
@@ -59,7 +59,9 @@ profile_to_acpi_platform_profile_value (PpdDriverPlatformProfile *self,
   case PPD_PROFILE_POWER_SAVER:
     if (g_strv_contains ((const char * const*) self->profile_choices, "low-power"))
       return "low-power";
-    return "cool";
+    if (g_strv_contains ((const char * const*) self->profile_choices, "cool"))
+      return "cool";
+    return "quiet";
   case PPD_PROFILE_BALANCED:
     return "balanced";
   case PPD_PROFILE_PERFORMANCE:
@@ -101,7 +103,7 @@ read_platform_profile (void)
 
   platform_profile_path = ppd_utils_get_sysfs_path (ACPI_PLATFORM_PROFILE_PATH);
   if (!g_file_get_contents (platform_profile_path,
-                            &new_profile_str, NULL, NULL)) {
+                            &new_profile_str, NULL, &error)) {
     g_debug ("Failed to get contents for '%s': %s",
              platform_profile_path,
              error->message);
@@ -124,7 +126,10 @@ save_platform_profile_choices (PpdDriverPlatformProfile *self)
 
   platform_profile_choices_path = ppd_utils_get_sysfs_path (ACPI_PLATFORM_PROFILE_CHOICES_PATH);
   if (!g_file_get_contents (platform_profile_choices_path,
-                            &choices_str, NULL, NULL)) {
+                            &choices_str, NULL, &error)) {
+    g_debug ("Failed to get contents for '%s': %s",
+             platform_profile_choices_path,
+             error->message);
     return FALSE;
   }
 
@@ -138,7 +143,8 @@ verify_acpi_platform_profile_choices (PpdDriverPlatformProfile *self)
   const char * const *choices = (const char * const*) self->profile_choices;
 
   if ((g_strv_contains (choices, "low-power") ||
-       g_strv_contains (choices, "cool")) &&
+       g_strv_contains (choices, "cool") ||
+       g_strv_contains (choices, "quiet")) &&
       g_strv_contains (choices, "balanced") &&
       g_strv_contains (choices, "performance"))
     return PPD_PROBE_RESULT_SUCCESS;
@@ -157,9 +163,9 @@ update_dytc_lapmode_state (PpdDriverPlatformProfile *self)
   self->lapmode = new_lapmode;
   g_debug ("dytc_lapmode is now %s, so profile is %s",
            self->lapmode ? "on" : "off",
-           self->lapmode ? "inhibited" : "uninhibited");
+           self->lapmode ? "degraded" : "not degraded");
   g_object_set (G_OBJECT (self),
-                "performance-inhibited", self->lapmode ? "lap-detected" : NULL,
+                "performance-degraded", self->lapmode ? "lap-detected" : NULL,
                 NULL);
 }
 
@@ -222,13 +228,6 @@ ppd_driver_platform_profile_activate_profile (PpdDriver                   *drive
     return TRUE;
   }
 
-  if (profile == PPD_PROFILE_PERFORMANCE &&
-      self->lapmode) {
-    g_debug ("Can't switch to performance mode, lapmode is detected");
-    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Mode is inhibited");
-    return FALSE;
-  }
-
   g_signal_handler_block (G_OBJECT (self->acpi_platform_profile_mon), self->acpi_platform_profile_changed_id);
   platform_profile_path = ppd_utils_get_sysfs_path (ACPI_PLATFORM_PROFILE_PATH);
   if (!ppd_utils_write (platform_profile_path, profile_to_acpi_platform_profile_value (self, profile), error)) {
@@ -257,8 +256,7 @@ find_dytc (GUdevDevice *dev,
 }
 
 static PpdProbeResult
-ppd_driver_platform_profile_probe (PpdDriver  *driver,
-                                   PpdProfile *prev_profile)
+ppd_driver_platform_profile_probe (PpdDriver  *driver)
 {
   PpdDriverPlatformProfile *self = PPD_DRIVER_PLATFORM_PROFILE (driver);
   g_autoptr(GFile) acpi_platform_profile = NULL;
@@ -293,8 +291,6 @@ ppd_driver_platform_profile_probe (PpdDriver  *driver,
     return self->probe_result;
   }
 
-  *prev_profile = read_platform_profile ();
-
   /* Lenovo-specific proximity sensor */
   self->device = ppd_utils_find_device ("platform",
                                         (GCompareFunc) find_dytc,
diff --git a/src/ppd-driver.c b/src/ppd-driver.c
index 8ccd305..93abb34 100644
--- a/src/ppd-driver.c
+++ b/src/ppd-driver.c
@@ -16,26 +16,22 @@
  * @Title: Profile Drivers
  *
  * Profile drivers are the implementation of the different profiles for
- * the whole system. A driver will implement support for one or more
- * profiles, usually one or both of the `performance` and `power-saver`
- * profiles, for a particular system. Only one driver will be selected and
- * running per profile.
+ * the whole system. A driver will need to implement support `power-saver`
+ * and `balanced` at a minimum.
  *
- * If no system-specific driver is available, some placeholder `balanced`
- * and `power-saver` drivers will be put in place, and the `performance`
- * profile will be unavailable.
+ * If no system-specific driver is available, a placeholder driver
+ * will be put in place, and the `performance` profile will be unavailable.
  *
- * Common implementation of drivers might be:
- * - a driver handling all three profiles, relying on a firmware feature
- *   exposed in the kernel,
- * - a driver that only implements the `performance` profile on a particular
- *   system it has intimate knowledge of, leaving the `balanced` and
- *   `power-saver` profiles using placeholder
+ * There should not be a need to implement system-specific drivers, as the
+ * [`platform_profile`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/ABI/testing/sysfs-platform_profile)
+ * kernel API offers a way to implement system-specific profiles which
+ * `power-profiles-daemon` can consume.
  *
  * When a driver implements the `performance` profile, it might set the
- * #PpdDriver:performance-inhibited property if the profile isn't available for any
- * reason, such as thermal limits being reached, or because a part of the
- * user's body is too close for safety, for example.
+ * #PpdDriver:performance-degraded property if the profile isn't running to
+ * its fullest performance for any reason, such as thermal limits being
+ * reached, or because a part of the user's body is too close for safety,
+ * for example.
  */
 
 typedef struct
@@ -43,14 +39,14 @@ typedef struct
   char          *driver_name;
   PpdProfile     profiles;
   gboolean       selected;
-  char          *performance_inhibited;
+  char          *performance_degraded;
 } PpdDriverPrivate;
 
 enum {
   PROP_0,
   PROP_DRIVER_NAME,
   PROP_PROFILES,
-  PROP_PERFORMANCE_INHIBITED
+  PROP_PERFORMANCE_DEGRADED
 };
 
 enum {
@@ -81,9 +77,9 @@ ppd_driver_set_property (GObject        *object,
   case PROP_PROFILES:
     priv->profiles = g_value_get_flags (value);
     break;
-  case PROP_PERFORMANCE_INHIBITED:
-    g_clear_pointer (&priv->performance_inhibited, g_free);
-    priv->performance_inhibited = g_value_dup_string (value);
+  case PROP_PERFORMANCE_DEGRADED:
+    g_clear_pointer (&priv->performance_degraded, g_free);
+    priv->performance_degraded = g_value_dup_string (value);
     break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
@@ -106,8 +102,8 @@ ppd_driver_get_property (GObject        *object,
   case PROP_PROFILES:
     g_value_set_flags (value, priv->profiles);
     break;
-  case PROP_PERFORMANCE_INHIBITED:
-    g_value_set_string (value, priv->performance_inhibited);
+  case PROP_PERFORMANCE_DEGRADED:
+    g_value_set_string (value, priv->performance_degraded);
     break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
@@ -121,7 +117,7 @@ ppd_driver_finalize (GObject *object)
 
   priv = PPD_DRIVER_GET_PRIVATE (PPD_DRIVER (object));
   g_clear_pointer (&priv->driver_name, g_free);
-  g_clear_pointer (&priv->performance_inhibited, g_free);
+  g_clear_pointer (&priv->performance_degraded, g_free);
 
   G_OBJECT_CLASS (ppd_driver_parent_class)->finalize (object);
 }
@@ -197,15 +193,15 @@ ppd_driver_class_init (PpdDriverClass *klass)
                                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
 
   /**
-   * PpdDriver:performance-inhibited:
+   * PpdDriver:performance-degraded:
    *
    * If set to a non-%NULL value, the reason why the performance profile is unavailable.
    * The value must be one of the options listed in the D-Bus API reference.
    */
-  g_object_class_install_property (object_class, PROP_PERFORMANCE_INHIBITED,
-                                   g_param_spec_string("performance-inhibited",
-                                                       "Performance Inhibited",
-                                                       "Why the performance profile is inhibited, if set",
+  g_object_class_install_property (object_class, PROP_PERFORMANCE_DEGRADED,
+                                   g_param_spec_string("performance-degraded",
+                                                       "Performance Degraded",
+                                                       "Why the performance profile is degraded, if set",
                                                        NULL,
                                                        G_PARAM_READWRITE));
 }
@@ -216,24 +212,14 @@ ppd_driver_init (PpdDriver *self)
 }
 
 PpdProbeResult
-ppd_driver_probe (PpdDriver  *driver,
-                  PpdProfile *previous_profile)
+ppd_driver_probe (PpdDriver  *driver)
 {
-  PpdProfile profile = PPD_PROFILE_UNSET;
-  PpdProbeResult ret;
-
   g_return_val_if_fail (PPD_IS_DRIVER (driver), FALSE);
-  g_return_val_if_fail (previous_profile != NULL, FALSE);
 
   if (!PPD_DRIVER_GET_CLASS (driver)->probe)
     return PPD_PROBE_RESULT_SUCCESS;
 
-  ret = PPD_DRIVER_GET_CLASS (driver)->probe (driver, &profile);
-  if (ret == PPD_PROBE_RESULT_SUCCESS &&
-      profile != PPD_PROFILE_UNSET &&
-      ppd_profile_has_single_flag (profile))
-    *previous_profile = profile;
-  return ret;
+  return PPD_DRIVER_GET_CLASS (driver)->probe (driver);
 }
 
 gboolean
@@ -285,18 +271,18 @@ ppd_driver_get_selected (PpdDriver *driver)
 }
 
 const char *
-ppd_driver_get_performance_inhibited (PpdDriver *driver)
+ppd_driver_get_performance_degraded (PpdDriver *driver)
 {
   PpdDriverPrivate *priv;
 
   g_return_val_if_fail (PPD_IS_DRIVER (driver), NULL);
 
   priv = PPD_DRIVER_GET_PRIVATE (driver);
-  return priv->performance_inhibited ? priv->performance_inhibited : "";
+  return priv->performance_degraded ? priv->performance_degraded : "";
 }
 
 gboolean
-ppd_driver_is_performance_inhibited (PpdDriver *driver)
+ppd_driver_is_performance_degraded (PpdDriver *driver)
 {
   PpdDriverPrivate *priv;
 
@@ -304,7 +290,7 @@ ppd_driver_is_performance_inhibited (PpdDriver *driver)
 
   priv = PPD_DRIVER_GET_PRIVATE (driver);
 
-  return (priv->performance_inhibited != NULL);
+  return (priv->performance_degraded != NULL);
 }
 
 void
@@ -323,14 +309,14 @@ const char *
 ppd_profile_activation_reason_to_str (PpdProfileActivationReason reason)
 {
   switch (reason) {
-  case PPD_PROFILE_ACTIVATION_REASON_INHIBITION:
-    return "inhibition";
   case PPD_PROFILE_ACTIVATION_REASON_INTERNAL:
     return "internal";
   case PPD_PROFILE_ACTIVATION_REASON_RESET:
     return "reset";
   case PPD_PROFILE_ACTIVATION_REASON_USER:
     return "user";
+  case PPD_PROFILE_ACTIVATION_REASON_PROGRAM_HOLD:
+    return "program-hold";
   default:
     g_assert_not_reached ();
   }
diff --git a/src/ppd-driver.h b/src/ppd-driver.h
index 7885c1e..16977b5 100644
--- a/src/ppd-driver.h
+++ b/src/ppd-driver.h
@@ -35,24 +35,24 @@ typedef enum {
 
 /**
  * PpdProfileActivationReason:
- * PPD_PROFILE_ACTIVATION_REASON_INHIBITION: switching profiles because
- *   of performance profile inhibition.
- * PPD_PROFILE_ACTIVATION_REASON_INTERNAL: the driver profile changed
+ * @PPD_PROFILE_ACTIVATION_REASON_INTERNAL: the driver profile changed
  *   internally, usually because of a key combination.
- * PPD_PROFILE_ACTIVATION_REASON_RESET: setting profile on startup, or
+ * @PPD_PROFILE_ACTIVATION_REASON_RESET: setting profile on startup, or
  *   because drivers are getting reprobed.
- * PPD_PROFILE_ACTIVATION_REASON_USER: setting profile because the user
+ * @PPD_PROFILE_ACTIVATION_REASON_USER: setting profile because the user
  *   requested it.
+ * @PPD_PROFILE_ACTIVATION_REASON_PROGRAM_HOLD: setting profile because a program
+ *   requested it through the `HoldProfile` method.
  *
  * Those are possible reasons for a profile being activated. Based on those
  * reasons, drivers can choose whether or not that changes the effective
  * profile internally.
  */
 typedef enum{
-  PPD_PROFILE_ACTIVATION_REASON_INHIBITION = 0,
-  PPD_PROFILE_ACTIVATION_REASON_INTERNAL,
+  PPD_PROFILE_ACTIVATION_REASON_INTERNAL = 0,
   PPD_PROFILE_ACTIVATION_REASON_RESET,
-  PPD_PROFILE_ACTIVATION_REASON_USER
+  PPD_PROFILE_ACTIVATION_REASON_USER,
+  PPD_PROFILE_ACTIVATION_REASON_PROGRAM_HOLD
 } PpdProfileActivationReason;
 
 /**
@@ -68,8 +68,7 @@ struct _PpdDriverClass
 {
   GObjectClass   parent_class;
 
-  PpdProbeResult (* probe)            (PpdDriver                   *driver,
-                                       PpdProfile                  *previous_profile);
+  PpdProbeResult (* probe)            (PpdDriver                   *driver);
   gboolean       (* activate_profile) (PpdDriver                   *driver,
                                        PpdProfile                   profile,
                                        PpdProfileActivationReason   reason,
@@ -77,13 +76,13 @@ struct _PpdDriverClass
 };
 
 #ifndef __GTK_DOC_IGNORE__
-PpdProbeResult ppd_driver_probe (PpdDriver *driver, PpdProfile *previous_profile);
+PpdProbeResult ppd_driver_probe (PpdDriver *driver);
 gboolean ppd_driver_activate_profile (PpdDriver *driver,
   PpdProfile profile, PpdProfileActivationReason reason, GError **error);
 const char *ppd_driver_get_driver_name (PpdDriver *driver);
 PpdProfile ppd_driver_get_profiles (PpdDriver *driver);
-const char *ppd_driver_get_performance_inhibited (PpdDriver *driver);
-gboolean ppd_driver_is_performance_inhibited (PpdDriver *driver);
+const char *ppd_driver_get_performance_degraded (PpdDriver *driver);
+gboolean ppd_driver_is_performance_degraded (PpdDriver *driver);
 void ppd_driver_emit_profile_changed (PpdDriver *driver, PpdProfile profile);
 const char *ppd_profile_activation_reason_to_str (PpdProfileActivationReason reason);
 #endif
diff --git a/tests/integration-test b/tests/integration-test.py
similarity index 58%
rename from tests/integration-test
rename to tests/integration-test.py
index 3ca5643..22dc42c 100755
--- a/tests/integration-test
+++ b/tests/integration-test.py
@@ -7,6 +7,7 @@
 #
 # Copyright: (C) 2011 Martin Pitt <martin.pitt at ubuntu.com>
 # (C) 2020 Bastien Nocera <hadess at hadess.net>
+# (C) 2021 David Redondo <kde at david-redondo.de>
 #
 # 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
@@ -50,6 +51,7 @@ except ImportError:
 
 PP = 'net.hadess.PowerProfiles'
 PP_PATH = '/net/hadess/PowerProfiles'
+PP_INTERFACE = 'net.hadess.PowerProfiles'
 
 class Tests(dbusmock.DBusTestCase):
     @classmethod
@@ -102,6 +104,10 @@ class Tests(dbusmock.DBusTestCase):
         The testbed is initially empty.
         '''
         self.testbed = UMockdev.Testbed.new()
+        self.polkitd, self.obj_polkit = self.spawn_server_template(
+            'polkitd', {}, stdout=subprocess.PIPE)
+        self.obj_polkit.SetAllowed(['net.hadess.PowerProfiles.switch-profile',
+                                    'net.hadess.PowerProfiles.hold-profile'])
 
         self.proxy = None
         self.log = None
@@ -113,7 +119,21 @@ class Tests(dbusmock.DBusTestCase):
     def tearDown(self):
         del self.testbed
         self.stop_daemon()
+
+        if self.polkitd:
+            try:
+                self.polkitd.kill()
+            except OSError:
+                pass
+            self.polkitd.wait()
+        self.polkitd = None
+        self.obj_polkit = None
+
         del self.tp_acpi
+        try:
+          os.remove(self.testbed.get_root_dir() + '/' + 'ppd_test_conf.ini')
+        except Exception:
+          pass
 
         # on failures, print daemon log
         errors = [x[1] for x in self._outcome.errors if x[1]]
@@ -195,6 +215,15 @@ class Tests(dbusmock.DBusTestCase):
             PP_PATH, 'org.freedesktop.DBus.Properties', None)
         return proxy.Set('(ssv)', PP, name, value)
 
+    def call_dbus_method(self, name, parameters):
+        '''Call a method of the daemon D-Bus interface.'''
+
+        proxy = Gio.DBusProxy.new_sync(
+            self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None, PP,
+            PP_PATH, PP_INTERFACE, None)
+        return proxy.call_sync(name, parameters, Gio.DBusCallFlags.NO_AUTO_START, -1, None)
+
+
     def have_text_in_log(self, text):
         return self.count_text_in_log(text) > 0
 
@@ -221,14 +250,28 @@ class Tests(dbusmock.DBusTestCase):
           [ 'DEVPATH', '/devices/platform/thinkpad_acpi' ]
       )
 
+    def create_empty_platform_profile(self):
+      acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+      os.makedirs(acpi_dir)
+      with open(os.path.join(acpi_dir, "platform_profile"),'w') as profile:
+        profile.write('\n')
+      with open(os.path.join(acpi_dir, "platform_profile_choices"),'w') as choices:
+        choices.write('\n')
+
     def create_platform_profile(self):
       acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
       os.makedirs(acpi_dir)
-      with open(os.path.join(acpi_dir, "platform_profile")  ,'w') as profile:
+      with open(os.path.join(acpi_dir, "platform_profile"),'w') as profile:
         profile.write("performance\n")
-      with open(os.path.join(acpi_dir, "platform_profile_choices")  ,'w') as choices:
+      with open(os.path.join(acpi_dir, "platform_profile_choices"),'w') as choices:
         choices.write("low-power balanced performance\n")
 
+    def remove_platform_profile(self):
+      acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+      os.remove(os.path.join(acpi_dir, "platform_profile_choices"))
+      os.remove(os.path.join(acpi_dir, "platform_profile"))
+      os.removedirs(acpi_dir)
+
     def assertEventually(self, condition, message=None, timeout=50):
         '''Assert that condition function eventually returns True.
 
@@ -253,7 +296,7 @@ class Tests(dbusmock.DBusTestCase):
       '''D-Bus startup error'''
 
       self.start_daemon()
-      out = subprocess.run([self.daemon_path], capture_output=True)
+      out = subprocess.run([self.daemon_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
       self.assertEqual(out.returncode, 1, "power-profile-daemon started but should have failed")
       self.stop_daemon()
 
@@ -262,7 +305,7 @@ class Tests(dbusmock.DBusTestCase):
 
       self.start_daemon()
       self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-      self.assertEqual(self.get_dbus_property('PerformanceInhibited'), '')
+      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), '')
 
       profiles = self.get_dbus_property('Profiles')
       self.assertEqual(len(profiles), 2)
@@ -279,8 +322,8 @@ class Tests(dbusmock.DBusTestCase):
 
       self.stop_daemon()
 
-    def test_inhibited_transition(self):
-      '''Test that transitions work as expected when inhibited'''
+    def test_inhibited_property(self):
+      '''Test that the inhibited property exists'''
 
       self.create_dytc_device()
       self.create_platform_profile()
@@ -288,17 +331,27 @@ class Tests(dbusmock.DBusTestCase):
 
       profiles = self.get_dbus_property('Profiles')
       self.assertEqual(len(profiles), 3)
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      self.assertEqual(self.get_dbus_property('PerformanceInhibited'), '')
+
+    def test_degraded_transition(self):
+      '''Test that transitions work as expected when degraded'''
+
+      self.create_dytc_device()
+      self.create_platform_profile()
+      self.start_daemon()
+
+      profiles = self.get_dbus_property('Profiles')
+      self.assertEqual(len(profiles), 3)
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
 
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('balanced'))
       self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
       self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
 
-      # Inhibit
+      # Degraded
       self.testbed.set_attribute(self.tp_acpi, 'dytc_lapmode', '1\n')
       self.assertEventually(lambda: self.have_text_in_log('dytc_lapmode is now on'))
-      self.assertEqual(self.get_dbus_property('PerformanceInhibited'), 'lap-detected')
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), 'lap-detected')
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
 
       # Switch to non-performance
       self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
@@ -310,17 +363,17 @@ class Tests(dbusmock.DBusTestCase):
       # Create 2 CPUs with preferences
       dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
       os.makedirs(dir1)
-      with open(os.path.join(dir1, "energy_performance_preference")  ,'w') as prefs:
+      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
         prefs.write("performance\n")
       dir2 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy1/")
       os.makedirs(dir2)
-      with open(os.path.join(dir2, "energy_performance_preference")  ,'w') as prefs:
+      with open(os.path.join(dir2, "energy_performance_preference"),'w') as prefs:
         prefs.write("performance\n")
 
       # Create no_turbo pref
       pstate_dir = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate")
       os.makedirs(pstate_dir)
-      with open(os.path.join(pstate_dir, "no_turbo")  ,'w') as no_turbo:
+      with open(os.path.join(pstate_dir, "no_turbo"),'w') as no_turbo:
         no_turbo.write("0\n")
 
       self.start_daemon()
@@ -345,12 +398,12 @@ class Tests(dbusmock.DBusTestCase):
       self.assertEqual(contents, b'performance')
 
       # Disable turbo
-      with open(os.path.join(pstate_dir, "no_turbo")  ,'w') as no_turbo:
+      with open(os.path.join(pstate_dir, "no_turbo"),'w') as no_turbo:
         no_turbo.write("1\n")
 
       self.assertEventually(lambda: self.have_text_in_log('File monitor change happened for '))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-      self.assertEqual(self.get_dbus_property('PerformanceInhibited'), 'high-operating-temperature')
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), 'high-operating-temperature')
 
       self.stop_daemon()
 
@@ -368,7 +421,7 @@ class Tests(dbusmock.DBusTestCase):
       # Create CPU with preference
       dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
       os.makedirs(dir1)
-      with open(os.path.join(dir1, "energy_performance_preference")  ,'w') as prefs:
+      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
         prefs.write("performance\n")
 
       upowerd, obj_upower = self.spawn_server_template(
@@ -406,33 +459,20 @@ class Tests(dbusmock.DBusTestCase):
       self.assertEqual(profiles[0]['Profile'], 'power-saver')
       self.assertEqual(profiles[2]['Driver'], 'platform_profile')
       self.assertEqual(profiles[2]['Profile'], 'performance')
+      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
       self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
 
-      # lapmode detected, but performance wasn't selected anyway
+      # lapmode detected
       self.testbed.set_attribute(self.tp_acpi, 'dytc_lapmode', '1\n')
-      self.assertEventually(lambda: self.get_dbus_property('PerformanceInhibited') == 'lap-detected')
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.assertEventually(lambda: self.get_dbus_property('PerformanceDegraded') == 'lap-detected')
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
 
       # Reset lapmode
       self.testbed.set_attribute(self.tp_acpi, 'dytc_lapmode', '0\n')
-      self.assertEventually(lambda: self.get_dbus_property('PerformanceInhibited') == '')
+      self.assertEventually(lambda: self.get_dbus_property('PerformanceDegraded') == '')
 
-      # Set performance mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
+      # Performance mode didn't change
       self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-      self.assertEventually(lambda: self.read_sysfs_file("sys/firmware/acpi/platform_profile") == b'performance')
-
-      # And turn on lapmode
-      self.testbed.set_attribute(self.tp_acpi, 'dytc_lapmode', '1\n')
-      self.assertEventually(lambda: self.read_sysfs_file("sys/firmware/acpi/platform_profile") == b'balanced')
-
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-      self.assertEqual(self.get_dbus_property('PerformanceInhibited'), 'lap-detected')
-
-      # Turn off lapmode, profile stays balanced
-      self.testbed.set_attribute(self.tp_acpi, 'dytc_lapmode', '0\n')
-      self.assertEventually(lambda: self.get_dbus_property('PerformanceInhibited') == '')
-      self.assertEventually(lambda: self.read_sysfs_file("sys/firmware/acpi/platform_profile") == b'balanced')
 
       # Switch to power-saver mode
       self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
@@ -488,21 +528,16 @@ class Tests(dbusmock.DBusTestCase):
 
     def test_platform_driver_late_load(self):
       '''Test that we can handle the platform_profile driver getting loaded late'''
-      acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
-      os.makedirs(acpi_dir)
-      with open(os.path.join(acpi_dir, "platform_profile")  ,'w') as profile:
-        profile.write('\n')
-      with open(os.path.join(acpi_dir, "platform_profile_choices")  ,'w') as choices:
-        choices.write('\n')
-
+      self.create_empty_platform_profile()
       self.start_daemon()
 
       profiles = self.get_dbus_property('Profiles')
       self.assertEqual(len(profiles), 2)
 
-      with open(os.path.join(acpi_dir, "platform_profile_choices")  ,'w') as choices:
+      acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+      with open(os.path.join(acpi_dir, "platform_profile_choices"),'w') as choices:
         choices.write("low-power\nbalanced\nperformance\n")
-      with open(os.path.join(acpi_dir, "platform_profile")  ,'w') as profile:
+      with open(os.path.join(acpi_dir, "platform_profile"),'w') as profile:
         profile.write("performance\n")
 
       # Wait for profiles to get reloaded
@@ -510,8 +545,8 @@ class Tests(dbusmock.DBusTestCase):
       profiles = self.get_dbus_property('Profiles')
       self.assertEqual(len(profiles), 3)
       # Was set in platform_profile before we loaded the drivers
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-      self.assertEqual(self.get_dbus_property('PerformanceInhibited'), '')
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), '')
 
       self.stop_daemon()
 
@@ -520,9 +555,9 @@ class Tests(dbusmock.DBusTestCase):
       # Uses cool instead of low-power
       acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
       os.makedirs(acpi_dir)
-      with open(os.path.join(acpi_dir, "platform_profile")  ,'w') as profile:
+      with open(os.path.join(acpi_dir, "platform_profile"),'w') as profile:
         profile.write("cool\n")
-      with open(os.path.join(acpi_dir, "platform_profile_choices")  ,'w') as choices:
+      with open(os.path.join(acpi_dir, "platform_profile_choices"),'w') as choices:
         choices.write("cool balanced performance\n")
 
       self.start_daemon()
@@ -530,14 +565,232 @@ class Tests(dbusmock.DBusTestCase):
       self.assertEqual(len(profiles), 3)
       self.assertEqual(profiles[0]['Driver'], 'platform_profile')
       self.assertEqual(profiles[0]['Profile'], 'power-saver')
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.assertEqual(self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b'balanced')
+      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
       self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
       self.assertEqual(self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b'cool')
 
-      # Check that we can set the power-saver/cool profile again
+      self.stop_daemon()
+
+    def test_quiet(self):
+      # Uses cool instead of low-power
+      acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+      os.makedirs(acpi_dir)
+      with open(os.path.join(acpi_dir, "platform_profile"),'w') as profile:
+        profile.write("cool\n")
+      with open(os.path.join(acpi_dir, "platform_profile_choices"),'w') as choices:
+        choices.write("quiet balanced balanced-performance performance\n")
+
+      self.start_daemon()
+      profiles = self.get_dbus_property('Profiles')
+      self.assertEqual(len(profiles), 3)
+      self.assertEqual(profiles[0]['Driver'], 'platform_profile')
+      self.assertEqual(profiles[0]['Profile'], 'power-saver')
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.assertEqual(self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b'balanced')
+      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.assertEqual(self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b'quiet')
+
+      self.stop_daemon()
+
+    def test_hold_release_profile(self):
+      self.create_platform_profile()
+      self.start_daemon()
+
+      profiles = self.get_dbus_property('Profiles')
+      self.assertEqual(len(profiles), 3)
+
+      cookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', 'testReason', 'testApplication')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      profileHolds = self.get_dbus_property('ActiveProfileHolds')
+      self.assertEqual(len(profileHolds), 1)
+      self.assertEqual(profileHolds[0]["Profile"], "performance")
+      self.assertEqual(profileHolds[0]["Reason"], "testReason")
+      self.assertEqual(profileHolds[0]["ApplicationId"], "testApplication")
+
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", cookie))
+      profileHolds = self.get_dbus_property('ActiveProfileHolds')
+      self.assertEqual(len(profileHolds), 0)
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      # When the profile is changed manually, holds should be released a
+      self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(len(self.get_dbus_property('ActiveProfileHolds')), 1)
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+
       self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('balanced'))
+      self.assertEqual(len(self.get_dbus_property('ActiveProfileHolds')), 0)
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      # When all holds are released, the last manually selected profile should be activated
       self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
       self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-      self.assertEqual(self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b'cool')
+      cookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", cookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+
+      self.stop_daemon()
+
+    def test_vanishing_hold(self):
+      self.create_platform_profile()
+      self.start_daemon()
+
+      builddir = os.getenv('top_builddir', '.')
+      tool_path = os.path.join(builddir, 'src', 'powerprofilesctl')
+
+      launch_process = subprocess.Popen([tool_path, 'launch', '-p', 'power-saver', 'sleep', '3600'],
+          stdout=sys.stdout, stderr=sys.stderr)
+      assert launch_process
+      time.sleep(1)
+      holds = self.get_dbus_property('ActiveProfileHolds')
+      self.assertEqual(len(holds), 1)
+      hold = holds[0]
+      self.assertEqual(hold['Profile'], 'power-saver')
+
+      # Make sure to handle vanishing clients
+      launch_process.terminate()
+      launch_process.wait()
+
+      holds = self.get_dbus_property('ActiveProfileHolds')
+      self.assertEqual(len(holds), 0)
+
+      self.stop_daemon()
+
+    def test_hold_priority(self):
+      '''power-saver should take priority over performance'''
+      self.create_platform_profile()
+      self.start_daemon()
+
+      profiles = self.get_dbus_property('Profiles')
+      self.assertEqual(len(profiles), 3)
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      # Test every order of holding and releasing power-saver and performance
+      # hold performance and then power-saver, release in the same order
+      performanceCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      powerSaverCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('power-saver', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", performanceCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", powerSaverCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      # hold performance and then power-saver, but release power-saver first
+      performanceCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      powerSaverCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('power-saver', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)",powerSaverCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", performanceCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      # hold power-saver and then performance, release in the same order
+      powerSaverCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('power-saver', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      performanceCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)",powerSaverCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", performanceCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      # hold power-saver and then performance, but release performance first
+      powerSaverCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('power-saver', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      performanceCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)",performanceCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.call_dbus_method('ReleaseProfile', GLib.Variant("(u)", powerSaverCookie))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      self.stop_daemon()
+
+    def test_save_profile(self):
+      '''save profile across runs'''
+
+      self.create_platform_profile()
+
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
+      self.stop_daemon()
+
+      # sys.stderr.write('\n-------------- config file: ----------------\n')
+      # with open(self.testbed.get_root_dir() + '/' + 'ppd_test_conf.ini') as f:
+      #   sys.stderr.write(f.read())
+      # sys.stderr.write('------------------------------\n')
+
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      # Programmatically set profile aren't saved
+      performanceCookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
+      self.stop_daemon()
+
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
+      self.stop_daemon()
+
+    def test_save_deferred_load(self):
+      '''save profile across runs, but kernel driver loaded after start'''
+
+      self.create_platform_profile()
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
+      self.stop_daemon()
+      self.remove_platform_profile()
+
+      # We could verify the contents of the configuration file here
+
+      self.create_empty_platform_profile()
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+      with open(os.path.join(acpi_dir, "platform_profile_choices"),'w') as choices:
+        choices.write("low-power\nbalanced\nperformance\n")
+      with open(os.path.join(acpi_dir, "platform_profile"),'w') as profile:
+        profile.write("performance\n")
+
+      self.assertEventually(lambda: self.get_dbus_property('ActiveProfile') == 'power-saver')
+      self.stop_daemon()
+
+    def test_not_allowed_profile(self):
+      '''Check that we get errors when trying to change a profile and not allowed'''
+
+      self.obj_polkit.SetAllowed(dbus.Array([], signature='s'))
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      proxy = Gio.DBusProxy.new_sync(
+          self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None, PP,
+          PP_PATH, 'org.freedesktop.DBus.Properties', None)
+      with self.assertRaises(gi.repository.GLib.GError) as cm:
+          proxy.Set('(ssv)', PP, 'ActiveProfile', GLib.Variant.new_string('power-saver'))
+      self.assertIn('AccessDenied', str(cm.exception))
+
+      self.stop_daemon()
+
+    def test_not_allowed_hold(self):
+      '''Check that we get an error when trying to hold a profile and not allowed'''
+
+      self.obj_polkit.SetAllowed(dbus.Array([], signature='s'))
+      self.start_daemon()
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+
+      with self.assertRaises(gi.repository.GLib.GError) as cm:
+        self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', '', '')))
+      self.assertIn('AccessDenied', str(cm.exception))
+
+      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
+      self.assertEqual(len(self.get_dbus_property('ActiveProfileHolds')), 0)
 
       self.stop_daemon()
 
diff --git a/tests/meson.build b/tests/meson.build
index 462c3aa..21d3152 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,10 +1,18 @@
-integration_test = find_program('integration-test')
-
 envs = environment()
 envs.set ('top_builddir', meson.build_root())
 envs.set ('top_srcdir', meson.source_root())
 
-test('power-profiles-daemon-integration-test',
-  integration_test,
-  env: envs
-  )
+python3 = find_program('python3')
+unittest_inspector = find_program('unittest_inspector.py')
+r = run_command(unittest_inspector, files('integration-test.py'))
+unit_tests = r.stdout().strip().split('\n')
+
+foreach ut: unit_tests
+    ut_args = files('integration-test.py')
+    ut_args += ut
+    test(ut,
+         python3,
+         args: ut_args,
+         env: envs,
+        )
+endforeach
diff --git a/tests/unittest_inspector.py b/tests/unittest_inspector.py
new file mode 100755
index 0000000..0d5d3a6
--- /dev/null
+++ b/tests/unittest_inspector.py
@@ -0,0 +1,46 @@
+#! /usr/bin/env python3
+# Copyright © 2020, Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+# Authors:
+#       Marco Trevisan <marco.trevisan at canonical.com>
+
+import argparse
+import importlib.util
+import inspect
+import os
+import unittest
+
+def list_tests(module):
+    tests = []
+    for name, obj in inspect.getmembers(module):
+        if inspect.isclass(obj) and issubclass(obj, unittest.TestCase):
+            cases = unittest.defaultTestLoader.getTestCaseNames(obj)
+            tests += [ (obj, '{}.{}'.format(name, t)) for t in cases ]
+    return tests
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('unittest_source', type=argparse.FileType('r'))
+
+    args = parser.parse_args()
+    source_path = args.unittest_source.name
+    spec = importlib.util.spec_from_file_location(
+        os.path.basename(source_path), source_path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+
+    for machine, human in list_tests(module):
+        print(human)



More information about the Neon-commits mailing list