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

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


Git commit 8448eb4d7e15231a4a59d04b8ddc4ce0be5af2ca by git-ubuntu importer, on behalf of Marco Trevisan (Treviño).
Committed on 15/02/2024 at 10:34.
Pushed by carlosdem into branch 'Neon/unstable'.

0.20-1 (patches unapplied)

Imported using git-ubuntu import.

M  +25   -11   .ci/fail_skipped_tests.py
M  +2    -1    .gitignore
M  +96   -10   .gitlab-ci.yml
A  +10   -0    .markdownlint.json
A  +39   -0    .pre-commit-config.yaml
M  +20   -0    NEWS
M  +84   -38   README.md
M  +8    -8    check-news.sh
M  +24   -16   data/meson.build
D  +0    -21   data/net.hadess.PowerProfiles.conf.in
A  +21   -0    data/power-profiles-daemon.dbus.conf.in
R  +1    -1    data/power-profiles-daemon.dbus.service.in [from: data/net.hadess.PowerProfiles.service - 091% similarity]
R  +2    -2    data/power-profiles-daemon.policy [from: data/net.hadess.PowerProfiles.policy - 087% similarity]
M  +1    -1    data/power-profiles-daemon.service.in
M  +22   -0    debian/changelog
M  +8    -4    debian/control
M  +10   -2    debian/copyright
A  +32   -0    debian/patches/build-Expose-powerprofilesctl-script-and-load-it-using-fi.patch
A  +108  -0    debian/patches/powerprofilectl-Generate-manpage-using-argparse-manpage.patch
M  +13   -5    debian/patches/remove_tlp_conflict.patch
M  +2    -0    debian/patches/series
M  +8    -3    debian/rules
M  +4    -4    debian/upstream/metadata
M  +1    -1    debian/watch
M  +5    -6    docs/meson.build
M  +4    -1    docs/power-profiles-daemon-docs.xml
M  +32   -0    docs/power-profiles-daemon-sections.txt
M  +36   -7    meson.build
M  +1    -1    meson_options.txt
M  +54   -18   src/meson.build
M  +482  -128  src/power-profiles-daemon.c
R  +10   -3    src/power-profiles-daemon.dbus.xml.in [from: src/net.hadess.PowerProfiles.xml - 095% similarity]
D  +0    -7    src/power-profiles-daemon.gresource.xml
A  +6    -0    src/power-profiles-daemon.gresource.xml.in
A  +227  -0    src/powerprofilesctl
D  +0    -282  src/powerprofilesctl.in
A  +350  -0    src/ppd-action-amdgpu-panel-power.c     [License: GPL (v3)]
A  +15   -0    src/ppd-action-amdgpu-panel-power.h     [License: GPL (v3)]
M  +4    -2    src/ppd-action-trickle-charge.c
M  +2    -2    src/ppd-action-trickle-charge.h
M  +8    -6    src/ppd-action.c
M  +4    -4    src/ppd-action.h
M  +86   -25   src/ppd-driver-amd-pstate.c
M  +3    -3    src/ppd-driver-amd-pstate.h
A  +47   -0    src/ppd-driver-cpu.c     [License: GPL (v3)]
A  +27   -0    src/ppd-driver-cpu.h     [License: GPL (v3)]
M  +11   -7    src/ppd-driver-fake.c
M  +3    -3    src/ppd-driver-fake.h
M  +22   -12   src/ppd-driver-intel-pstate.c
M  +3    -3    src/ppd-driver-intel-pstate.h
M  +5    -3    src/ppd-driver-placeholder.c
M  +3    -3    src/ppd-driver-placeholder.h
M  +5    -3    src/ppd-driver-platform-profile.c
M  +3    -3    src/ppd-driver-platform-profile.h
A  +58   -0    src/ppd-driver-platform.c     [License: GPL (v3)]
A  +27   -0    src/ppd-driver-platform.h     [License: GPL (v3)]
M  +17   -18   src/ppd-driver.c
M  +5    -22   src/ppd-driver.h
M  +18   -0    src/ppd-profile.h
M  +14   -1    src/ppd-utils.c
M  +4    -0    src/ppd-utils.h
D  +0    -1264 tests/integration-test.py
A  +1993 -0    tests/integration_test.py
M  +75   -7    tests/meson.build
M  +6    -4    tests/unittest_inspector.py

https://invent.kde.org/neon/backports-jammy/power-profiles-daemon/-/commit/8448eb4d7e15231a4a59d04b8ddc4ce0be5af2ca

diff --git a/.ci/fail_skipped_tests.py b/.ci/fail_skipped_tests.py
index 6349921..868fd06 100755
--- a/.ci/fail_skipped_tests.py
+++ b/.ci/fail_skipped_tests.py
@@ -3,23 +3,37 @@
 from lxml import etree
 import sys
 
+
 def format_title(title):
     """Put title in a box"""
     box = {
-        'tl': '╔', 'tr': '╗', 'bl': '╚', 'br': '╝', 'h': '═', 'v': '║',
+        "tl": "╔",
+        "tr": "╗",
+        "bl": "╚",
+        "br": "╝",
+        "h": "═",
+        "v": "║",
     }
-    hline = box['h'] * (len(title) + 2)
+    hline = box["h"] * (len(title) + 2)
+
+    return "\n".join(
+        [
+            f"{box['tl']}{hline}{box['tr']}",
+            f"{box['v']} {title} {box['v']}",
+            f"{box['bl']}{hline}{box['br']}",
+        ]
+    )
 
-    return '\n'.join([
-        f"{box['tl']}{hline}{box['tr']}",
-        f"{box['v']} {title} {box['v']}",
-        f"{box['bl']}{hline}{box['br']}",
-    ])
 
 tree = etree.parse(sys.argv[1])
-for suite in tree.xpath('/testsuites/testsuite'):
-    skipped = suite.get('skipped')
+for suite in tree.xpath("/testsuites/testsuite"):
+    skipped = suite.get("skipped")
     if int(skipped) != 0:
-        print(format_title('Tests were skipped when they should not have been. All the tests must be run in the CI'),
-                end='\n\n', flush=True)
+        print(
+            format_title(
+                "Tests were skipped when they should not have been. All the tests must be run in the CI"
+            ),
+            end="\n\n",
+            flush=True,
+        )
         sys.exit(1)
diff --git a/.gitignore b/.gitignore
index b8dbfe9..f07a5a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 power-profiles-daemon
-data/net.hadess.PowerProfiles.conf
+__pycache__
+.vscode
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ddec8d0..81fc905 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,7 @@ image: fedora:rawhide
 
 variables:
   DEPENDENCIES: gcc
+                gcovr
                 gtk-doc
                 pkgconfig(udev)
                 pkgconfig(systemd)
@@ -14,6 +15,7 @@ variables:
                 git
                 python3-gobject
                 python3-dbusmock
+                python3-packaging
                 python3-pylint
                 umockdev
                 e2fsprogs
@@ -23,21 +25,105 @@ workflow:
     - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
     - if: $CI_PIPELINE_SOURCE == 'push'
 
-build_stable:
+.install-deps:
+  variables:
+    TMPDIR: $CI_BUILDS_DIR/tmpdir
   before_script:
-    - dnf upgrade -y --nogpgcheck fedora-release fedora-repos*
-    - dnf update -y && dnf install -y $DEPENDENCIES
-    - mkdir tmpdir/
+    - echo 8096000 > /proc/sys/fs/inotify/max_user_instances
+    - mkdir -m 700 $TMPDIR -p
+    - dnf update -y
+    - if [ -x /bin/dnf ]; then
+        dnf install -y $DEPENDENCIES $JOB_DEPS;
+      else
+        dnf5 install -y $DEPENDENCIES $JOB_DEPS;
+      fi
+
+pre_commit:
+  variables:
+    DEPENDENCIES: {}
+    JOB_DEPS: pre-commit
+              git
+  extends:
+    - .install-deps
+  script:
+    - pre-commit run --all-files
+
+build_stable:
+  extends:
+    - .install-deps
   script:
-    - meson -Dgtk_doc=true -Dpylint=true -Dtests=true _build
-    - ninja -v -C _build
-    - ninja -v -C _build install
-    - ninja -v -C _build uninstall
-    - TMPDIR=$(pwd)/tmpdir meson test -C _build
+    - meson setup
+        --werror
+        --fatal-meson-warnings
+        --warnlevel 2
+        -Dgtk_doc=true
+        -Dpylint=true
+        -Db_coverage=true
+        _build
+    - meson test -C _build --print-errorlogs
     - .ci/fail_skipped_tests.py _build/meson-logs/testlog.junit.xml
-    - TMPDIR=$(pwd)/tmpdir ninja -v -C _build dist
+    - ninja -C _build coverage
+    - cat _build/meson-logs/coverage.txt || true
+    - meson install -C _build
+    - ninja -C _build uninstall -v
+    - meson dist -C _build
   artifacts:
     when: always
     paths:
     - _build/meson-logs/*.txt
     - _build/meson-dist/*
+    - _build/meson-logs/coveragereport/index.html
+    reports:
+      junit:
+        - _build/meson-logs/testlog.junit.xml
+      coverage_report:
+        coverage_format: cobertura
+        path: _build/meson-logs/coverage.xml
+  coverage: '/^TOTAL.*\s+(\d+\%)$/'
+
+address_sanitizer:
+  variables:
+    JOB_DEPS: libasan
+              libubsan
+  extends:
+    - .install-deps
+  script:
+    - meson setup
+        --werror
+        --buildtype=debug
+        _build
+        -Db_sanitize=address,undefined
+    - meson test -C _build --print-errorlogs -t 3
+  artifacts:
+    when: on_failure
+    paths:
+    - _build/meson-logs/*.txt
+
+valgrind:
+  variables:
+    JOB_DEPS: valgrind
+  extends:
+    - .install-deps
+  script:
+    - meson setup
+        --werror
+        --buildtype=debug
+        _build
+    - meson test -C _build --print-errorlogs --setup=valgrind
+  artifacts:
+    when: on_failure
+    paths:
+    - _build/meson-logs/*.txt
+
+scan_build:
+  variables:
+    JOB_DEPS: clang-analyzer
+  extends:
+    - .install-deps
+  script:
+    - meson setup _build
+    -  env SCANBUILD=$(which scan-build) ninja -C _build scan-build
+  artifacts:
+    when: on_failure
+    paths:
+      - _build/meson-logs
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..3d29c36
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,10 @@
+{
+  "default": true,
+  "MD033": false,
+  "MD041": false,
+  "MD036": false,
+  "MD013": {
+    "tables": false,
+    "line_length": 1000
+  }
+}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..b44d998
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,39 @@
+default_stages: [commit]
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+    -   id: no-commit-to-branch
+        args: [--branch, main]
+    -   id: check-added-large-files
+    -   id: check-byte-order-marker
+    -   id: check-executables-have-shebangs
+    -   id: forbid-new-submodules
+    -   id: check-yaml
+    -   id: check-json
+    -   id: pretty-format-json
+        args: ['--no-sort-keys']
+    -   id: check-symlinks
+    -   id: check-xml
+    -   id: end-of-file-fixer
+        types_or: [c, shell, python]
+    -   id: trailing-whitespace
+        types_or: [c, shell, python, xml]
+    -   id: check-docstring-first
+    -   id: check-merge-conflict
+    -   id: mixed-line-ending
+        args: [--fix=lf]
+-   repo: https://github.com/codespell-project/codespell
+    rev: v2.2.6
+    hooks:
+    -   id: codespell
+        args: ['--write-changes']
+-   repo: https://github.com/ambv/black
+    rev: 23.12.0
+    hooks:
+    - id: black
+-   repo: https://github.com/igorshubovych/markdownlint-cli
+    rev: v0.38.0
+    hooks:
+    - id: markdownlint
+      args: ['--fix']
diff --git a/NEWS b/NEWS
index 59ce68c..247eb8d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,23 @@
+0.20
+----
+
+The project has moved under the freedesktop 'Upower' group. The service is
+now advertised as 'org.freedesktop.UPower.PowerProfiles' in addition to the
+previous 'net.hadess.PowerProfiles' for compatibility reasons.
+
+This release adds support for:
+
+* Multiple power-profiles-daemon drivers to load simultaneously.  This notably
+  allows both CPU based control with amd-pstate or intel-pstate as well as
+  ACPI platform profile based control.
+
+* amdgpu panel power savings which uses dedicated hardware in systems with
+  integrated Radeon graphics to decrease panel power consumption when the
+  system is on battery.
+
+This release also enables the test suite by default, so distribution vendors
+should update packaging accordingly.
+
 0.13
 ----
 
diff --git a/README.md b/README.md
index 8ab25a3..90e032c 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,17 @@
-power-profiles-daemon
-=====================
+# power-profiles-daemon
 
 Makes power profiles handling available over D-Bus.
 
-Installation
-------------
+## Installation
+
 ```sh
-$ meson _build -Dprefix=/usr
-$ ninja -v -C _build install
+meson setup _build -Dprefix=/usr
+ninja -C _build install
 ```
+
 It requires libgudev, systemd and polkit-gobject.
 
-Introduction
-------------
+## Introduction
 
 power-profiles-daemon offers to modify system behaviour based upon user-selected
 power profiles. There are 3 different power profiles, a "balanced" default mode,
@@ -30,8 +29,7 @@ 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.
 
-How to use
-----------
+## How to use
 
 There are interfaces to switch profiles in the latest versions of KDE and GNOME. Those
 desktops also include more thorough integration with its low-power mode. Please check
@@ -44,6 +42,7 @@ or the power-saver profile.
 
 For example, this will be useful to avoid manual switching profiles while compiling
 large projects:
+
 ```sh
 powerprofilesctl launch make
 ```
@@ -52,8 +51,7 @@ If you're a developer, you might also want to use GLib's [`GPowerProfileMonitor`
 through C, or one of its bindings, so your application can react to the user requesting
 a low-power mode.
 
-Conflicts
----------
+## Conflicts
 
 If `power-profiles-daemon` refuses to start, it's likely that you have [a conflicting
 service installed and running](data/power-profiles-daemon.service.in#L3), or your
@@ -65,8 +63,7 @@ systemctl unmask power-profiles-daemon.service
 systemctl start power-profiles-daemon.service
 ```
 
-Debugging
----------
+## Debugging
 
 You can now check which mode is in use, and which ones are available by running:
 
@@ -86,9 +83,13 @@ 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'>"
+```sh
+gdbus introspect --system --dest org.freedesktop.UPower.PowerProfiles \
+  --object-path /org/freedesktop/UPower/PowerProfiles
+gdbus call --system --dest org.freedesktop.UPower.PowerProfiles \
+  --object-path /org/freedesktop/UPower/PowerProfiles \
+  --method org.freedesktop.DBus.Properties.Set 'org.freedesktop.UPower.PowerProfiles' \
+  'ActiveProfile' "<'power-saver'>"
 ```
 
 If that doesn't work, please file an issue, attach the output of:
@@ -97,19 +98,19 @@ If that doesn't work, please file an issue, attach the output of:
 sudo G_MESSAGES_DEBUG=all /usr/libexec/power-profiles-daemon -r -v
 ```
 
-Operations on Intel-based machines
-----------------------------------
+## Operations on Intel-based machines
 
 The "driver" for making the hardware act on the user-selected power profile on Intel
 CPU-based machines is based on the [Intel P-State scaling driver](https://www.kernel.org/doc/html/v5.17/admin-guide/pm/intel_pstate.html)
 or the Energy Performance Bias (EPB) feature if available.
 
-It is only used if a `platform_profile` driver isn't available for the system, and the
-CPU supports either hardware-managed P-states (HWP) or Energy Performance Bias (EPB).
+It is only used if the CPU supports either hardware-managed P-states (HWP)
+or Energy Performance Bias (EPB).
 
 Example of a system without `platform_profile support` but with `active` P-State
 operation mode:
-```
+
+```sh
 $ cat /sys/firmware/acpi/platform_profile_choices
 cat: /sys/firmware/acpi/platform_profile_choices: No such file or directory
 $ cat /sys/devices/system/cpu/intel_pstate/status
@@ -117,13 +118,14 @@ active
 ```
 
 Example of a system with `EPB` support:
-```
+
+```sh
 $ cat /sys/devices/system/cpu/cpu0/power/energy_perf_bias
 0
 ```
 
 If the Intel P-State scaling driver is in `passive` mode, either because the system doesn't
-support HWP, or the administator has disabled it, and `EPB` isn't available, then the
+support HWP, or the administrator has disabled it, and `EPB` isn't available, then the
 placeholder driver will be used, and there won't be a performance mode.
 
 Finally, if the Intel P-State scaling driver is used in `active` mode, the P-State
@@ -134,20 +136,22 @@ ie. the only P-State scaling governor that allows HWP to work.
 For more information, please refer to the [Intel P-State scaling driver documentation](https://www.kernel.org/doc/html/v5.17/admin-guide/pm/intel_pstate.html)
 and the [Intel Performance and Energy Bias Hint](https://www.kernel.org/doc/html/v5.17/admin-guide/pm/intel_epb.html).
 
-Operations on AMD-based machines
-----------------------------------
+## Operations on AMD-based machines
+
+### CPU power savings
 
 The "driver" for making the hardware act on the user-selected power profile on AMD CPU-based
 machines is based on the [AMD P-State scaling driver](https://www.kernel.org/doc/html/v6.3/admin-guide/pm/amd-pstate.html)
 if available.
 
-It is only used if a `platform_profile` driver isn't available for the system, the
-CPU supports Collaborative Processor Performance Control (CPPC), and the AMD P-State
-scaling driver is in `active` mode.
+It is only used if the CPU supports Collaborative Processor Performance
+Control (CPPC), the machine is a laptop or workstation and the
+AMD P-State scaling driver is in `active` mode.
 
 Example of a system without `platform_profile` support but with `active` P-State
 operation mode:
-```
+
+```sh
 $ cat /sys/firmware/acpi/platform_profile_choices
 cat: /sys/firmware/acpi/platform_profile_choices: No such file or directory
 $ cat /sys/devices/system/cpu/amd_pstate/status
@@ -163,8 +167,52 @@ governor that allows for the "Energy vs Performance Hints" to be taken into cons
 
 For more information, please refer to the [AMD P-State scaling driver documentation](https://www.kernel.org/doc/html/v6.3/admin-guide/pm/amd-pstate.html).
 
-Testing
--------
+### Panel power savings
+
+Laptops with integrated Radeon graphics have a dedicated hardware function
+to decrease panel power consumption in exchange for color accuracy. This
+function is used when the system is on battery and the user has selected
+the "balanced" or "power-saver" profiles.
+
+If you decide that you don't like how this behaves, you can disable the function
+in one of two ways:
+
+1. Adding `amdgpu.abmlevel=0` to the kernel command line.  This will disable abm
+   value changes entirely.
+2. By using `POWER_PROFILE_DAEMON_ACTION_BLOCK=amdgpu_panel_power` in the
+   `power-profiles-daemon` environment as described below. This will allow you to
+   still change values manually in sysfs but `power-profiles-daemon` will not
+   change anything.
+
+## Multiple driver and multiple action operations
+
+Power-profiles daemon will load all supported drivers and actions by default.
+If you have a problem with a given driver or action, you can disable it by
+populating the `POWER_PROFILE_DAEMON_DRIVER_BLOCK` or `POWER_PROFILE_DAEMON_ACTION_BLOCK`
+environment variables with the name of the driver or action you want to disable
+in the environment that launches the daemon (such as the systemd unit file).
+
+For example to edit the unit:
+
+```sh
+sudo systemctl edit power-profiles-daemon.service
+```
+
+Then add to the drop-in file:
+
+```text
+[Service]
+Environment=POWER_PROFILE_DAEMON_DRIVER_BLOCK=xxx
+Environment=POWER_PROFILE_DAEMON_ACTION_BLOCK=yyy
+```
+
+Then restart the service:
+
+```sh
+sudo systemctl try-restart power-profiles-daemon.service
+```
+
+## Testing
 
 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
@@ -174,8 +222,7 @@ variable `POWER_PROFILE_DAEMON_FAKE_DRIVER` set to 1. For example:
 sudo POWER_PROFILE_DAEMON_FAKE_DRIVER=1 /usr/libexec/power-profiles-daemon -r -v
 ```
 
-References
-----------
+## References
 
 - [Use Low Power Mode to save battery life on your iPhone (iOS)](https://support.apple.com/en-us/HT205234)
 - [lowPowerModeEnabled (iOS)](https://developer.apple.com/documentation/foundation/nsprocessinfo/1617047-lowpowermodeenabled?language=objc)
@@ -183,8 +230,7 @@ References
 - [[S]ettings that use less battery (Android)](https://support.google.com/android/answer/7664692?hl=en&visit_id=637297348326801871-2263015427&rd=1)
 - [EnergySaverStatus Enum (Windows)](https://docs.microsoft.com/en-us/uwp/api/windows.system.power.energysaverstatus?view=winrt-19041)
 
-Why power-profiles-daemon
--------------------------
+## Why power-profiles-daemon
 
 The power-profiles-daemon project was created to help provide a solution for
 two separate use cases, for desktops, laptops, and other devices running a
@@ -208,8 +254,7 @@ and make its API available over D-Bus, as has been customary for more than
 10 years. We would also design that API to be as easily usable to build
 graphical interfaces as possible.
 
-Why not...
-----------
+## Why not
 
 This section will contain explanations of why this new daemon was written
 rather than re-using, or modifying an existing one. Each project obviously
@@ -251,6 +296,7 @@ of goes against a user's wishes as a user might still want to conserve as
 much energy as possible under high-CPU usage.
 
 ### [slimbookbattery](https://launchpad.net/~slimbook)
+
 This is **not** free software (*Source code available but not modifiable
 without express authorization.*). The application does a lot of things in
 addition to the "3 profiles" selection:
diff --git a/check-news.sh b/check-news.sh
index 11ae861..cd6f819 100644
--- a/check-news.sh
+++ b/check-news.sh
@@ -24,13 +24,13 @@
 #
 # Checks NEWS for the version number:
 # meson.add_dist_script(
-#   find_program('check-news.sh').path(),
+#   find_program('check-news.sh').full_path(),
 #   '@0@'.format(meson.project_version())
 # )
 #
 # Checks NEWS and data/foo.appdata.xml for the version number:
 # meson.add_dist_script(
-#   find_program('check-news.sh').path(),
+#   find_program('check-news.sh').full_path(),
 #   '@0@'.format(meson.project_version()),
 #   'NEWS',
 #   'data/foo.appdata.xml'
@@ -48,9 +48,9 @@ check_version()
 	# Look in the first 15 lines for NEWS files, but look
 	# everywhere for other types of files
 	if [ "$2" = "NEWS" ]; then
-		DATA=`sed 15q $SRC_ROOT/"$2"`
+		DATA=$(sed 15q "$SRC_ROOT/$2")
 	else
-		DATA=`cat $SRC_ROOT/"$2"`
+		DATA=$(cat "$SRC_ROOT/$2")
 	fi
 	case "$DATA" in
 	*"$VERSION"*)
@@ -71,12 +71,12 @@ VERSION=$1
 shift
 
 if [ $# -eq 0 ] ; then
-	check_version $VERSION 'NEWS'
+	check_version "$VERSION" 'NEWS'
 	exit 0
 fi
 
-for i in $@ ; do
-	check_version $VERSION "$i"
+for i in "$@"; do
+	check_version "$VERSION" "$i"
 done
 
-exit 0
\ No newline at end of file
+exit 0
diff --git a/data/meson.build b/data/meson.build
index 3d1b466..a42e80f 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -1,32 +1,40 @@
-data_conf = configuration_data()
-data_conf.set('libexecdir', libexecdir)
-
 configure_file(
   input: 'power-profiles-daemon.service.in',
   output: 'power-profiles-daemon.service',
-  configuration: data_conf,
+  configuration: {
+    'libexecdir': libexecdir,
+  },
   install_dir: systemd_system_unit_dir,
 )
 
-configure_file(
-  input: 'net.hadess.PowerProfiles.conf.in',
-  output: 'net.hadess.PowerProfiles.conf',
-  configuration: data_conf,
-  install_dir: dbusconfdir
-)
+foreach name, _: bus_names
+  config = {
+    'dbus_name': name,
+    'dbus_iface': name,
+  }
 
-install_data(
-  'net.hadess.PowerProfiles.service',
-  install_dir: dbusservicedir
-)
+  configure_file(
+    input: 'power-profiles-daemon.dbus.conf.in',
+    output: name + '.conf',
+    configuration: config,
+    install_dir: dbusconfdir
+  )
+
+  configure_file(
+    input: 'power-profiles-daemon.dbus.service.in',
+    output: name + '.service',
+    configuration: config,
+    install_dir: dbusservicedir
+  )
+endforeach
 
-polkit_policy = 'net.hadess.PowerProfiles.policy'
+polkit_policy = 'power-profiles-daemon.policy'
 if xmllint.found()
   test(polkit_policy,
        xmllint,
        args: [
            '--noout',
-           meson.source_root() / 'data' / polkit_policy,
+           meson.project_source_root() / 'data' / polkit_policy,
        ])
 endif
 
diff --git a/data/net.hadess.PowerProfiles.conf.in b/data/net.hadess.PowerProfiles.conf.in
deleted file mode 100644
index a01d2d4..0000000
--- a/data/net.hadess.PowerProfiles.conf.in
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->
-
-<!DOCTYPE busconfig PUBLIC
- "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
- "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
-<busconfig>
-
-  <!-- Only root can own the service -->
-  <policy user="root">
-    <allow own="net.hadess.PowerProfiles"/>
-  </policy>
-
-  <!-- Anyone can talk to the main interface -->
-  <policy context="default">
-    <allow send_destination="net.hadess.PowerProfiles" send_interface="net.hadess.PowerProfiles"/>
-    <allow send_destination="net.hadess.PowerProfiles" send_interface="org.freedesktop.DBus.Introspectable"/>
-    <allow send_destination="net.hadess.PowerProfiles" send_interface="org.freedesktop.DBus.Properties"/>
-    <allow send_destination="net.hadess.PowerProfiles" send_interface="org.freedesktop.DBus.Peer"/>
-  </policy>
-
-</busconfig>
diff --git a/data/power-profiles-daemon.dbus.conf.in b/data/power-profiles-daemon.dbus.conf.in
new file mode 100644
index 0000000..844277f
--- /dev/null
+++ b/data/power-profiles-daemon.dbus.conf.in
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->
+
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+
+  <!-- Only root can own the service -->
+  <policy user="root">
+    <allow own="@dbus_name@"/>
+  </policy>
+
+  <!-- Anyone can talk to the main interface -->
+  <policy context="default">
+    <allow send_destination="@dbus_name@" send_interface="@dbus_iface@"/>
+    <allow send_destination="@dbus_name@" send_interface="org.freedesktop.DBus.Introspectable"/>
+    <allow send_destination="@dbus_name@" send_interface="org.freedesktop.DBus.Properties"/>
+    <allow send_destination="@dbus_name@" send_interface="org.freedesktop.DBus.Peer"/>
+  </policy>
+
+</busconfig>
diff --git a/data/net.hadess.PowerProfiles.service b/data/power-profiles-daemon.dbus.service.in
similarity index 91%
rename from data/net.hadess.PowerProfiles.service
rename to data/power-profiles-daemon.dbus.service.in
index 724f631..5baefa1 100644
--- a/data/net.hadess.PowerProfiles.service
+++ b/data/power-profiles-daemon.dbus.service.in
@@ -5,7 +5,7 @@
 # the Free Software Foundation.
 
 [D-BUS Service]
-Name=net.hadess.PowerProfiles
+Name=@dbus_name@
 Exec=/bin/false
 User=root
 SystemdService=power-profiles-daemon.service
diff --git a/data/net.hadess.PowerProfiles.policy b/data/power-profiles-daemon.policy
similarity index 87%
rename from data/net.hadess.PowerProfiles.policy
rename to data/power-profiles-daemon.policy
index a332b42..722e758 100644
--- a/data/net.hadess.PowerProfiles.policy
+++ b/data/power-profiles-daemon.policy
@@ -8,7 +8,7 @@
   <vendor>power-profiles-daemon</vendor>
   <vendor_url>https://gitlab.freedesktop.org/hadess/power-profiles-daemon</vendor_url>
 
-  <action id="net.hadess.PowerProfiles.switch-profile">
+  <action id="org.freedesktop.UPower.PowerProfiles.switch-profile">
     <description>Switch Power Profile</description>
     <message>Privileges are required to switch power profiles.</message>
     <defaults>
@@ -18,7 +18,7 @@
     </defaults>
   </action>
 
-  <action id="net.hadess.PowerProfiles.hold-profile">
+  <action id="org.freedesktop.UPower.PowerProfiles.hold-profile">
     <description>Hold Power Profile</description>
     <message>Privileges are required to hold power profiles.</message>
     <defaults>
diff --git a/data/power-profiles-daemon.service.in b/data/power-profiles-daemon.service.in
index dcd3503..17ec1d2 100644
--- a/data/power-profiles-daemon.service.in
+++ b/data/power-profiles-daemon.service.in
@@ -5,7 +5,7 @@ Before=multi-user.target display-manager.target
 
 [Service]
 Type=dbus
-BusName=net.hadess.PowerProfiles
+BusName=org.freedesktop.UPower.PowerProfiles
 ExecStart=@libexecdir@/power-profiles-daemon
 Restart=on-failure
 # This always corresponds to /var/lib/power-profiles-daemon
diff --git a/debian/changelog b/debian/changelog
index 3ae1b0e..b8094bf 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,25 @@
+power-profiles-daemon (0.20-1) unstable; urgency=medium
+
+  * New upstream release:
+    - New default bus name is org.freedesktop.UPower.PowerProfiles
+    - Support multiple drivers (ACPI + amd/intel ones)
+  * debian/watch: Update reference to new upstream repository
+  * debian: Update references to new repository under UPower namespace
+  * debian/copyright: Update copyright to new maintainers
+  * debian/control: Add myself to uploaders
+  * debian/control: Add python dependencies
+    While the daemon does not require python at runtime, the control tool
+    does so ensure debian picks them
+  * debian/patches: Refresh
+  * debian/rules: drop dh_missing override for --fail-missing.
+    It's default for some time now
+  * debian/patches: Add upstream patch to generate powerprofilectl manfile
+  * debian/control: Add manpage generation dependencies
+  * debian/rules: Explicitly enable manpage feature
+  * debian/rules: Fix generated manpage entries
+
+ -- Marco Trevisan (Treviño) <marco at ubuntu.com>  Thu, 15 Feb 2024 05:38:30 +0100
+
 power-profiles-daemon (0.13-2) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/debian/control b/debian/control
index ffffaba..6e0008f 100644
--- a/debian/control
+++ b/debian/control
@@ -2,8 +2,9 @@ Source: power-profiles-daemon
 Section: admin
 Priority: optional
 Maintainer: Debian freedesktop.org maintainers <pkg-freedesktop-maintainers at lists.alioth.debian.org>
-Uploaders: Sebastien Bacher <seb128 at ubuntu.com>
+Uploaders: Sebastien Bacher <seb128 at ubuntu.com>, Marco Trevisan (Treviño) <marco at ubuntu.com>
 Build-Depends: debhelper-compat (= 13),
+               dh-python,
                libglib2.0-dev,
                libgudev-1.0-dev,
                libpolkit-gobject-1-dev,
@@ -12,19 +13,22 @@ Build-Depends: debhelper-compat (= 13),
                libumockdev-dev,
                libxml2-utils,
                meson,
+               python3,
+               python3-argparse-manpage,
                python3-dbus <!nocheck>,
                python3-dbusmock <!nocheck>,
-               python3-gi <!nocheck>,
+               python3-gi,
                systemd,
                umockdev <!nocheck>,
 Standards-Version: 4.6.2
 Vcs-Browser: https://salsa.debian.org/freedesktop-team/power-profiles-daemon
 Vcs-Git: https://salsa.debian.org/freedesktop-team/power-profiles-daemon.git
-Homepage: https://gitlab.freedesktop.org/hadess/power-profiles-daemon
+Homepage: https://gitlab.freedesktop.org/upower/power-profiles-daemon
+X-Python3-Version: >= 3.11
 
 Package: power-profiles-daemon
 Architecture: linux-any
-Depends: ${misc:Depends}, ${shlibs:Depends},
+Depends: ${misc:Depends}, ${shlibs:Depends}, ${python3:Depends}, python3-gi
 Description: Makes power profiles handling available over D-Bus.
  power-profiles-daemon offers to modify system behaviour based upon
  user-selected power profiles. There are 3 different power profiles, a
diff --git a/debian/copyright b/debian/copyright
index 24ea099..d82f806 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,18 +1,26 @@
 Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
 Upstream-Name: power-profiles-daemon
-Source: https://gitlab.freedesktop.org/hadess/power-profiles-daemon
+Source: https://gitlab.freedesktop.org/upower/power-profiles-daemon
 
 Files: *
 Copyright: 2014-2016, 2020 Bastien Nocera <hadess at hadess.net>
+           2024 Marco Trevisan <marco at ubuntu.com>
+           2024 Mario Limonciello <mario.limonciello at amd.com>
 License: GPL-3
 
 Files: debian/*
 Copyright: 2021 Sebastien Bacher <seb128 at ubuntu.com>
 License: GPL-3
 
-Files: tests/integration-test
+Files: tests/integration_test.py
 Copyright: 2011 Martin Pitt <martin.pitt at ubuntu.com>
            2020 Bastien Nocera <hadess at hadess.net>
+           2024 Marco Trevisan <marco at ubuntu.com>
+           2024 Mario Limonciello <mario.limonciello at amd.com>
+License: GPL-2+
+
+Files: tests/unittest_inspector.py
+Copyright: Marco Trevisan <marco.trevisan at canonical.com>
 License: GPL-2+
 
 Files: docs/power-profiles-daemon-docs.xml
diff --git a/debian/patches/build-Expose-powerprofilesctl-script-and-load-it-using-fi.patch b/debian/patches/build-Expose-powerprofilesctl-script-and-load-it-using-fi.patch
new file mode 100644
index 0000000..36ceed9
--- /dev/null
+++ b/debian/patches/build-Expose-powerprofilesctl-script-and-load-it-using-fi.patch
@@ -0,0 +1,32 @@
+From: =?utf-8?b?Ik1hcmNvIFRyZXZpc2FuIChUcmV2acOxbyki?= <mail at 3v1n0.net>
+Date: Thu, 15 Feb 2024 03:22:52 +0100
+Subject: build: Expose powerprofilesctl script and load it using files
+
+Origin: https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/merge_requests/163
+---
+ src/meson.build | 7 +++----
+ 1 file changed, 3 insertions(+), 4 deletions(-)
+
+diff --git a/src/meson.build b/src/meson.build
+index 88b8ff6..3f59dd3 100644
+--- a/src/meson.build
++++ b/src/meson.build
+@@ -104,15 +104,14 @@ executable('power-profiles-daemon',
+   install_dir: libexecdir
+ )
+ 
+-script = 'powerprofilesctl'
+-install_data(script,
++powerprofilesctl = files('powerprofilesctl')
++install_data(powerprofilesctl,
+              install_dir: get_option('bindir')
+ )
+-script = join_paths(meson.current_source_dir(), script)
+ if get_option('pylint')
+   test('pylint-powerprofilesctl',
+        pylint,
+-       args: pylint_flags + [ script ],
++       args: pylint_flags + [ powerprofilesctl ],
+        env: nomalloc,
+        )
+ endif
diff --git a/debian/patches/powerprofilectl-Generate-manpage-using-argparse-manpage.patch b/debian/patches/powerprofilectl-Generate-manpage-using-argparse-manpage.patch
new file mode 100644
index 0000000..fdd0858
--- /dev/null
+++ b/debian/patches/powerprofilectl-Generate-manpage-using-argparse-manpage.patch
@@ -0,0 +1,108 @@
+From: =?utf-8?b?Ik1hcmNvIFRyZXZpc2FuIChUcmV2acOxbyki?= <mail at 3v1n0.net>
+Date: Thu, 15 Feb 2024 04:05:37 +0100
+Subject: powerprofilectl: Generate manpage using argparse-manpage
+
+Add an option to toggle this feature so that it can be either
+required or not
+
+Origin: https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/merge_requests/163
+---
+ meson.build          |  5 +++++
+ meson_options.txt    |  4 ++++
+ src/meson.build      | 31 +++++++++++++++++++++++++++++++
+ src/powerprofilesctl |  8 ++++++--
+ 4 files changed, 46 insertions(+), 2 deletions(-)
+
+diff --git a/meson.build b/meson.build
+index cd35078..1a4d116 100644
+--- a/meson.build
++++ b/meson.build
+@@ -56,6 +56,11 @@ if get_option('pylint')
+ endif
+ xmllint = find_program('xmllint', required: false)
+ 
++manpage = get_option('manpage')
++if not manpage.disabled()
++  argparse_manpage = find_program('argparse-manpage', required: manpage.enabled())
++endif
++
+ bus_names = {
+   'org.freedesktop.UPower.PowerProfiles': '/org/freedesktop/UPower/PowerProfiles',
+   'net.hadess.PowerProfiles': '/net/hadess/PowerProfiles',
+diff --git a/meson_options.txt b/meson_options.txt
+index 5e9a7ce..0de978c 100644
+--- a/meson_options.txt
++++ b/meson_options.txt
+@@ -14,3 +14,7 @@ option('tests',
+        description: 'Whether to run tests',
+        type: 'boolean',
+        value: true)
++option('manpage',
++       description: 'gemerate powerprofilesctl man page',
++       type: 'feature',
++       value: 'auto')
+diff --git a/src/meson.build b/src/meson.build
+index 3f59dd3..65ddcc4 100644
+--- a/src/meson.build
++++ b/src/meson.build
+@@ -115,3 +115,34 @@ if get_option('pylint')
+        env: nomalloc,
+        )
+ endif
++
++if not manpage.disabled() and argparse_manpage.found()
++  argparse_features = run_command(argparse_manpage, '--help',
++    check: true).stdout().strip()
++
++  install_man(configure_file(
++    command: [
++      argparse_manpage,
++      '--pyfile', powerprofilesctl,
++      '--function', 'get_parser',
++      argparse_features.contains('--author') ?
++        ['--author', 'Bastien Nocera'] : [],
++      argparse_features.contains('--author-email') ?
++        ['--author-email', 'hadess at hadess.net'] : [],
++      argparse_features.contains('--project-name') ?
++        ['--project-name', meson.project_name()] : [],
++      argparse_features.contains('--version') ?
++        ['--version', meson.project_version()] : [],
++      argparse_features.contains('--url') ?
++        ['--url', 'https://gitlab.freedesktop.org/upower/power-profiles-daemon'] : [],
++      argparse_features.contains('--description') ?
++        ['--manual-title', 'Power Profiles Daemon Control Program'] : [],
++      argparse_features.contains('--description') ?
++        ['--description', 'Command line utility to control Power Profiles Daemon'] : [],
++      argparse_features.contains('--format') ?
++        ['--format', 'single-commands-section'] : [],
++    ],
++    capture: true,
++    output: 'powerprofilesctl.1',
++  ))
++endif
+diff --git a/src/powerprofilesctl b/src/powerprofilesctl
+index af4dfe1..051538d 100755
+--- a/src/powerprofilesctl
++++ b/src/powerprofilesctl
+@@ -164,7 +164,7 @@ def _launch(args):
+     return ret
+ 
+ 
+-def main():
++def get_parser():
+     parser = argparse.ArgumentParser(
+         epilog="Use “powerprofilesctl COMMAND --help” to get detailed help for individual commands",
+     )
+@@ -216,7 +216,11 @@ def main():
+     )
+     parser_version.set_defaults(func=_version)
+ 
+-    args = parser.parse_args()
++    return parser
++
++
++def main():
++    args = get_parser().parse_args()
+     # default behavior is to run list if no command is given
+     if not args.command:
+         args.func = _list
diff --git a/debian/patches/remove_tlp_conflict.patch b/debian/patches/remove_tlp_conflict.patch
index e334e26..5aced1f 100644
--- a/debian/patches/remove_tlp_conflict.patch
+++ b/debian/patches/remove_tlp_conflict.patch
@@ -1,13 +1,21 @@
-Description: remove the systemd unit conflict on tlp.service
+From: "Debian freedesktop.org maintainers"
+ <pkg-freedesktop-maintainers at lists.alioth.debian.org>
+Date: Thu, 15 Feb 2024 04:23:10 +0100
+Subject: remove the systemd unit conflict on tlp.service
+
 we don't want that in Ubuntu since we patch tlp to not do performance modes
 changes when power-profiles-daemon is active but the change can be
 included in Debian also since the tlp maintainer made the packages conflict
 which means we can't end up installed together
 forwarded: not-needed
-Index: power-profiles-daemon/data/power-profiles-daemon.service.in
-===================================================================
---- power-profiles-daemon.orig/data/power-profiles-daemon.service.in
-+++ power-profiles-daemon/data/power-profiles-daemon.service.in
+---
+ data/power-profiles-daemon.service.in | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/data/power-profiles-daemon.service.in b/data/power-profiles-daemon.service.in
+index 17ec1d2..2b913fa 100644
+--- a/data/power-profiles-daemon.service.in
++++ b/data/power-profiles-daemon.service.in
 @@ -1,6 +1,6 @@
  [Unit]
  Description=Power Profiles daemon
diff --git a/debian/patches/series b/debian/patches/series
index a0d0401..ee18d0d 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +1,3 @@
 remove_tlp_conflict.patch
+build-Expose-powerprofilesctl-script-and-load-it-using-fi.patch
+powerprofilectl-Generate-manpage-using-argparse-manpage.patch
diff --git a/debian/rules b/debian/rules
index 9573e7d..6494fef 100755
--- a/debian/rules
+++ b/debian/rules
@@ -4,7 +4,12 @@ export DEB_BUILD_MAINT_OPTIONS = hardening=+all
 export DEB_LDFLAGS_MAINT_APPEND = -Wl,-z,defs
 
 %:
-	dh $@
+	dh $@ --with python3
 
-override_dh_missing:
-	dh_missing --fail-missing
+override_dh_auto_configure:
+	dh_auto_configure -- \
+		-Dmanpage=enabled
+	# Fix contents of manpage, can be dropped when newer argparse-manpage lands
+	# in debian
+	sed -i s,argparse-manpage,powerprofilesctl,g \
+		$(CURDIR)/obj-$(DEB_HOST_GNU_TYPE)/src/powerprofilesctl.1
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
index b105363..dd970b0 100644
--- a/debian/upstream/metadata
+++ b/debian/upstream/metadata
@@ -1,5 +1,5 @@
 ---
-Bug-Database: https://gitlab.freedesktop.org/hadess/power-profiles-daemon/-/issues
-Bug-Submit: https://gitlab.freedesktop.org/hadess/power-profiles-daemon/-/issues/new
-Repository: https://gitlab.freedesktop.org/hadess/power-profiles-daemon.git
-Repository-Browse: https://gitlab.freedesktop.org/hadess/power-profiles-daemon
+Bug-Database: https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/issues
+Bug-Submit: https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/issues/new
+Repository: https://gitlab.freedesktop.org/upower/power-profiles-daemon.git
+Repository-Browse: https://gitlab.freedesktop.org/upower/power-profiles-daemon
diff --git a/debian/watch b/debian/watch
index 506172d..e845cda 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,3 +1,3 @@
 version=4
-https://gitlab.freedesktop.org/hadess/power-profiles-daemon/tags \
+https://gitlab.freedesktop.org/upower/power-profiles-daemon/tags \
 	.*/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@
diff --git a/docs/meson.build b/docs/meson.build
index 4de0210..ead4574 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -11,8 +11,8 @@ content_files += configure_file(
 
 content_files += gnome.gdbus_codegen(
   meson.project_name(),
-  sources: meson.source_root() / 'src' / 'net.hadess.PowerProfiles.xml',
-  interface_prefix: 'net.hadess',
+  sources: dbus_xml['org.freedesktop.UPower.PowerProfiles'],
+  interface_prefix: 'org.freedesktop.UPower',
   namespace: 'PowerProfiles',
   docbook: 'docs',
   build_by_default: true,
@@ -21,13 +21,12 @@ content_files += gnome.gdbus_codegen(
 private_headers = [
   'power-profiles-daemon.h',
   'ppd-action-trickle-charge.h',
-  'ppd-driver-balanced.h',
+  'ppd-action-amdgpu-panel-power.h',
   'ppd-driver-fake.h',
   'ppd-driver-intel-pstate.h',
   'ppd-driver-amd-pstate.h',
   'ppd-driver-placeholder.h',
   'ppd-driver-platform-profile.h',
-  'ppd-driver-power-saver.h',
   'ppd-utils.h',
   'power-profiles-daemon-resources.h',
 ]
@@ -39,8 +38,8 @@ gnome.gtkdoc(
   dependencies: libpower_profiles_daemon_dep,
   gobject_typesfile: [join_paths(meson.current_source_dir(), 'power-profiles-daemon.types')],
   src_dir: [
-    meson.source_root() /'src',
-    meson.build_root() / 'src',
+    meson.project_source_root() /'src',
+    meson.project_build_root() / 'src',
   ],
   ignore_headers: private_headers,
 )
diff --git a/docs/power-profiles-daemon-docs.xml b/docs/power-profiles-daemon-docs.xml
index 0a2c627..e903706 100644
--- a/docs/power-profiles-daemon-docs.xml
+++ b/docs/power-profiles-daemon-docs.xml
@@ -63,14 +63,17 @@
 	Power Profiles daemon.
       </para>
     </partintro>
-    <xi:include href="docs-net.hadess.PowerProfiles.xml"/>
+    <xi:include href="docs-org.freedesktop.UPower.PowerProfiles.xml"/>
   </reference>
 
   <reference id="internal-api">
     <title>Internal API</title>
     <xi:include href="xml/ppd-profile.xml"/>
     <xi:include href="xml/ppd-driver.xml"/>
+    <xi:include href="xml/ppd-driver-cpu.xml"/>
+    <xi:include href="xml/ppd-driver-platform.xml"/>
     <xi:include href="xml/ppd-action.xml"/>
+    <xi:include href="xml/ppd-action-amdgpu-panel-power.xml"/>
   </reference>
 
   <index>
diff --git a/docs/power-profiles-daemon-sections.txt b/docs/power-profiles-daemon-sections.txt
index 0063310..3c88528 100644
--- a/docs/power-profiles-daemon-sections.txt
+++ b/docs/power-profiles-daemon-sections.txt
@@ -1,3 +1,10 @@
+<SECTION>
+<FILE>config</FILE>
+<SUBSECTION Private>
+VERSION
+</SECTION>
+</SECTION>
+
 <SECTION>
 <FILE>ppd-action</FILE>
 <TITLE>Profile Actions</TITLE>
@@ -7,6 +14,15 @@ PpdAction
 PPD_TYPE_ACTION
 </SECTION>
 
+<SECTION>
+<FILE>ppd-action-amdgpu-panel-power</FILE>
+<TITLE>AMDGPU Power Panel Saving Action</TITLE>
+PpdActionAmdgpuPanelPowerClass
+_PpdActionAmdgpuPanelPower
+<SUBSECTION Private>
+PPD_TYPE_ACTION
+</SECTION>
+
 <SECTION>
 <FILE>ppd-driver</FILE>
 <TITLE>Profile Drivers</TITLE>
@@ -16,6 +32,22 @@ PpdProbeResult
 PpdProfileActivationReason
 <SUBSECTION Private>
 PPD_TYPE_DRIVER
+PPD_TYPE_DRIVER_CPU
+PPD_TYPE_DRIVER_PLATFORM
+</SECTION>
+
+<SECTION>
+<FILE>ppd-driver-cpu</FILE>
+<TITLE>CPU Profile Drivers</TITLE>
+PpdDriverCpuClass
+PpdDriverCpu
+</SECTION>
+
+<SECTION>
+<FILE>ppd-driver-platform</FILE>
+<TITLE>Platform Profile Drivers</TITLE>
+PpdDriverPlatformClass
+PpdDriverPlatform
 </SECTION>
 
 <SECTION>
diff --git a/meson.build b/meson.build
index 47e982b..cd35078 100644
--- a/meson.build
+++ b/meson.build
@@ -1,12 +1,12 @@
 project('power-profiles-daemon', [ 'c' ],
-        version: '0.13',
+        version: '0.20',
         license: 'GPLv3+',
         default_options: [
           'buildtype=debugoptimized',
           'warning_level=1',
           'c_std=c99',
         ],
-        meson_version: '>= 0.54.0')
+        meson_version: '>= 0.59.0')
 
 cc = meson.get_compiler('c')
 
@@ -17,7 +17,12 @@ common_cflags = cc.get_supported_arguments([
     '-Wstrict-prototypes',
     '-Werror-implicit-function-declaration',
     '-Wno-pointer-sign',
-    '-Wshadow'
+    '-Wshadow',
+    '-Wno-sign-compare',
+    '-Wno-cast-function-type',
+    '-Wno-unused-parameter',
+    '-Wno-missing-field-initializers',
+    '-Wno-type-limits',
 ])
 
 prefix = get_option('prefix')
@@ -29,13 +34,15 @@ dbusservicedir = get_option('datadir') / 'dbus-1' / 'system-services'
 systemd_system_unit_dir = get_option('systemdsystemunitdir')
 if systemd_system_unit_dir == 'auto'
     systemd_dep = dependency('systemd')
-    systemd_system_unit_dir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir')
+    systemd_system_unit_dir = systemd_dep.get_variable('systemdsystemunitdir')
 endif
+glib_dep = dependency('glib-2.0')
+gio_unix_dep = dependency('gio-unix-2.0')
 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.114')
-polkit_policy_directory = polkit_gobject_dep.get_pkgconfig_variable('policydir')
+polkit_gobject_dep = dependency('polkit-gobject-1', version: '>= 0.91')
+polkit_policy_directory = polkit_gobject_dep.get_variable('policydir')
 
 gnome = import('gnome')
 
@@ -49,6 +56,15 @@ if get_option('pylint')
 endif
 xmllint = find_program('xmllint', required: false)
 
+bus_names = {
+  'org.freedesktop.UPower.PowerProfiles': '/org/freedesktop/UPower/PowerProfiles',
+  'net.hadess.PowerProfiles': '/net/hadess/PowerProfiles',
+}
+
+address_sanitizer = get_option('b_sanitize') == 'address' or \
+  get_option('b_sanitize') == 'address,undefined' or \
+  get_option('b_sanitize') == 'leak'
+
 subdir('src')
 subdir('data')
 
@@ -65,6 +81,11 @@ endif
 if get_option('tests')
   # Python 3 required modules
   python3_required_modules = ['dbusmock', 'gi']
+  gi_required_modules = {
+    'GLib': '2.0',
+    'Gio': '2.0',
+    'UMockdev': '1.0',
+  }
 
   python = import('python')
   python3 = python.find_installation('python3')
@@ -76,10 +97,18 @@ if get_option('tests')
     endif
   endforeach
 
+  foreach module, version : gi_required_modules
+    script = 'import gi; gi.require_version("@0@", "@1@")'.format(module, version)
+    if run_command(python3, '-c', script, check: false).returncode() != 0
+      error('''GObject Introspection module '@0@' version @1@ required for running tests but not found'''.format(
+        module, version))
+    endif
+  endforeach
+
   subdir('tests')
 endif
 
 meson.add_dist_script(
-  find_program('check-news.sh').path(),
+  find_program('check-news.sh').full_path(),
   '@0@'.format(meson.project_version())
 )
diff --git a/meson_options.txt b/meson_options.txt
index 307bc6a..5e9a7ce 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -13,4 +13,4 @@ option('pylint',
 option('tests',
        description: 'Whether to run tests',
        type: 'boolean',
-       value: false)
+       value: true)
diff --git a/src/meson.build b/src/meson.build
index 6cf5113..88b8ff6 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,17 +1,59 @@
-deps = [ gio_dep, gudev_dep, upower_dep, polkit_gobject_dep ]
+deps = [
+  gio_dep,
+  gio_unix_dep,
+  gudev_dep,
+  polkit_gobject_dep,
+  upower_dep,
+]
 
 config_h = configuration_data()
 config_h.set_quoted('VERSION', meson.project_version())
+config_h.set('POLKIT_HAS_AUTOPOINTERS', polkit_gobject_dep.version().version_compare('>= 0.114'))
 config_h_files = configure_file(
   output: 'config.h',
   configuration: config_h
 )
 
+dbus_xml = {}
+dbus_xml_sources = []
+resources_contents = []
+
+foreach name, path: bus_names
+  config = {
+    'dbus_name': name,
+    'dbus_iface': name,
+    'dbus_path': path,
+  }
+
+  xml_source = configure_file(
+    input: 'power-profiles-daemon.dbus.xml.in',
+    output: name + '.xml',
+    configuration: config,
+  )
+  dbus_xml_sources += xml_source
+  dbus_xml += {name: xml_source}
+
+  resources_contents += '<file preprocess="xml-stripblanks">@0 at .xml</file>'.format(name)
+endforeach
+
+resources_xml = configure_file(
+  input: 'power-profiles-daemon.gresource.xml.in',
+  output: '@BASENAME@',
+  configuration: {
+    'prefix': bus_names['org.freedesktop.UPower.PowerProfiles'],
+    'contents': '\n'.join(resources_contents),
+  },
+)
+
 resources = gnome.compile_resources(
-    'power-profiles-daemon-resources', 'power-profiles-daemon.gresource.xml',
-    c_name: 'power_profiles_daemon',
-    source_dir: '.',
-    export: true
+  'power-profiles-daemon-resources', resources_xml,
+  c_name: 'power_profiles_daemon',
+  dependencies: dbus_xml_sources,
+  source_dir: [
+    meson.current_source_dir(),
+    meson.current_build_dir(),
+  ],
+  export: true
 )
 
 sources = [
@@ -19,6 +61,8 @@ sources = [
   'ppd-utils.c',
   'ppd-action.c',
   'ppd-driver.c',
+  'ppd-driver-cpu.c',
+  'ppd-driver-platform.c',
   resources,
 ]
 
@@ -45,6 +89,7 @@ libpower_profiles_daemon_dep = declare_dependency(
 sources += [
   'power-profiles-daemon.c',
   'ppd-action-trickle-charge.c',
+  'ppd-action-amdgpu-panel-power.c',
   'ppd-driver-intel-pstate.c',
   'ppd-driver-amd-pstate.c',
   'ppd-driver-platform-profile.c',
@@ -59,20 +104,11 @@ executable('power-profiles-daemon',
   install_dir: libexecdir
 )
 
-python = import('python')
-py_installation = python.find_installation('python3', required: true)
-
-ppd_conf = configuration_data()
-ppd_conf.set('VERSION', meson.project_version())
-ppd_conf.set('PYTHON3', py_installation.path())
-
-script = configure_file(
-  input: 'powerprofilesctl.in',
-  output: 'powerprofilesctl',
-  configuration: ppd_conf,
-  install_dir: get_option('bindir')
+script = 'powerprofilesctl'
+install_data(script,
+             install_dir: get_option('bindir')
 )
-
+script = join_paths(meson.current_source_dir(), script)
 if get_option('pylint')
   test('pylint-powerprofilesctl',
        pylint,
diff --git a/src/power-profiles-daemon.c b/src/power-profiles-daemon.c
index 2a3c26b..eb673cf 100644
--- a/src/power-profiles-daemon.c
+++ b/src/power-profiles-daemon.c
@@ -8,26 +8,45 @@
  *
  */
 
+#define G_LOG_DOMAIN "Core"
+
 #include "config.h"
 
+#include <glib-unix.h>
 #include <locale.h>
 #include <polkit/polkit.h>
+#include <stdio.h>
 
 #include "power-profiles-daemon-resources.h"
 #include "power-profiles-daemon.h"
-#include "ppd-driver.h"
+#include "ppd-driver-cpu.h"
+#include "ppd-driver-platform.h"
 #include "ppd-action.h"
 #include "ppd-enums.h"
 
-#define POWER_PROFILES_DBUS_NAME          "net.hadess.PowerProfiles"
-#define POWER_PROFILES_DBUS_PATH          "/net/hadess/PowerProfiles"
+#define POWER_PROFILES_DBUS_NAME          "org.freedesktop.UPower.PowerProfiles"
+#define POWER_PROFILES_DBUS_PATH          "/org/freedesktop/UPower/PowerProfiles"
 #define POWER_PROFILES_IFACE_NAME         POWER_PROFILES_DBUS_NAME
 
+#define POWER_PROFILES_LEGACY_DBUS_NAME   "net.hadess.PowerProfiles"
+#define POWER_PROFILES_LEGACY_DBUS_PATH   "/net/hadess/PowerProfiles"
+#define POWER_PROFILES_LEGACY_IFACE_NAME  POWER_PROFILES_LEGACY_DBUS_NAME
+
+#define POWER_PROFILES_POLICY_NAMESPACE "org.freedesktop.UPower.PowerProfiles"
+
+#define POWER_PROFILES_RESOURCES_PATH "/org/freedesktop/UPower/PowerProfiles"
+
+#ifndef POLKIT_HAS_AUTOPOINTERS
+/* FIXME: Remove this once we're fine to depend on polkit 0.114 */
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (PolkitAuthorizationResult, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (PolkitSubject, g_object_unref)
+#endif
+
 typedef struct {
   GMainLoop *main_loop;
-  GDBusNodeInfo *introspection_data;
   GDBusConnection *connection;
   guint name_id;
+  guint legacy_name_id;
   gboolean was_started;
   int ret;
 
@@ -39,9 +58,11 @@ typedef struct {
   PpdProfile active_profile;
   PpdProfile selected_profile;
   GPtrArray *probed_drivers;
-  PpdDriver *driver;
+  PpdDriverCpu *cpu_driver;
+  PpdDriverPlatform *platform_driver;
   GPtrArray *actions;
   GHashTable *profile_holds;
+  GLogLevelFlags log_level;
 } PpdApp;
 
 typedef struct {
@@ -49,6 +70,7 @@ typedef struct {
   char *reason;
   char *application_id;
   char *requester;
+  char *requester_iface;
 } ProfileHold;
 
 static void
@@ -59,6 +81,7 @@ profile_hold_free (ProfileHold *hold)
   g_free (hold->reason);
   g_free (hold->application_id);
   g_free (hold->requester);
+  g_free (hold->requester_iface);
   g_free (hold);
 }
 
@@ -67,11 +90,9 @@ static PpdApp *ppd_app = NULL;
 static void stop_profile_drivers (PpdApp *data);
 static void start_profile_drivers (PpdApp *data);
 
-#define GET_DRIVER(p) (ppd_driver_get_profiles (data->driver) & p ? data->driver : NULL)
-#define ACTIVE_DRIVER (data->driver)
-
 /* profile drivers and actions */
 #include "ppd-action-trickle-charge.h"
+#include "ppd-action-amdgpu-panel-power.h"
 #include "ppd-driver-placeholder.h"
 #include "ppd-driver-platform-profile.h"
 #include "ppd-driver-intel-pstate.h"
@@ -92,6 +113,7 @@ static GTypeGetFunc objects[] = {
 
   /* Actions */
   ppd_action_trickle_charge_get_type,
+  ppd_action_amdgpu_panel_power_get_type,
 };
 
 typedef enum {
@@ -100,19 +122,33 @@ typedef enum {
   PROP_PROFILES                   = 1 << 2,
   PROP_ACTIONS                    = 1 << 3,
   PROP_DEGRADED                   = 1 << 4,
-  PROP_ACTIVE_PROFILE_HOLDS       = 1 << 5
+  PROP_ACTIVE_PROFILE_HOLDS       = 1 << 5,
+  PROP_VERSION                    = 1 << 6,
 } PropertiesMask;
 
-#define PROP_ALL (PROP_ACTIVE_PROFILE | PROP_INHIBITED | PROP_PROFILES | PROP_ACTIONS | PROP_DEGRADED | PROP_ACTIVE_PROFILE_HOLDS)
+#define PROP_ALL (PROP_ACTIVE_PROFILE       | \
+                  PROP_INHIBITED            | \
+                  PROP_PROFILES             | \
+                  PROP_ACTIONS              | \
+                  PROP_DEGRADED             | \
+                  PROP_ACTIVE_PROFILE_HOLDS | \
+                  PROP_VERSION)
+
+static gboolean
+driver_profile_support (PpdDriver *driver,
+                       PpdProfile profile)
+{
+  if (!PPD_IS_DRIVER (driver))
+    return FALSE;
+  return (ppd_driver_get_profiles (driver) & profile) != 0;
+}
 
 static gboolean
 get_profile_available (PpdApp     *data,
                        PpdProfile  profile)
 {
-    PpdDriver *driver;
-
-    driver = GET_DRIVER(profile);
-    return driver != NULL;
+  return driver_profile_support (PPD_DRIVER (data->cpu_driver), profile) ||
+         driver_profile_support (PPD_DRIVER (data->platform_driver), profile);
 }
 
 static const char *
@@ -121,18 +157,28 @@ get_active_profile (PpdApp *data)
   return ppd_profile_to_str (data->active_profile);
 }
 
-static const char *
+static char *
 get_performance_degraded (PpdApp *data)
 {
-  const char *ret;
-  PpdDriver *driver;
-
-  driver = GET_DRIVER(PPD_PROFILE_PERFORMANCE);
-  if (!driver)
-    return "";
-  ret = ppd_driver_get_performance_degraded (driver);
-  g_assert (ret != NULL);
-  return ret;
+  const gchar *cpu_degraded = NULL;
+  const gchar *platform_degraded = NULL;
+
+  if (driver_profile_support (PPD_DRIVER (data->platform_driver), PPD_PROFILE_PERFORMANCE))
+    platform_degraded = ppd_driver_get_performance_degraded (PPD_DRIVER (data->platform_driver));
+
+  if (driver_profile_support (PPD_DRIVER (data->cpu_driver), PPD_PROFILE_PERFORMANCE))
+    cpu_degraded = ppd_driver_get_performance_degraded (PPD_DRIVER (data->cpu_driver));
+
+  if (!cpu_degraded && !platform_degraded)
+    return g_strdup ("");
+
+  if (!cpu_degraded)
+    return g_strdup (platform_degraded);
+
+  if (!platform_degraded)
+    return g_strdup (cpu_degraded);
+
+  return g_strjoin (",", cpu_degraded, platform_degraded, NULL);
 }
 
 static GVariant *
@@ -144,17 +190,40 @@ get_profiles_variant (PpdApp *data)
   g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
 
   for (i = 0; i < NUM_PROFILES; i++) {
-    PpdDriver *driver = GET_DRIVER(1 << i);
+    PpdDriver *platform_driver = PPD_DRIVER (data->platform_driver);
+    PpdDriver *cpu_driver = PPD_DRIVER (data->cpu_driver);
+    PpdProfile profile = 1 << i;
     GVariantBuilder asv_builder;
+    gboolean cpu, platform;
+    const gchar *driver = NULL;
 
-    if (driver == NULL)
+    /* check if any of the drivers support */
+    if (!get_profile_available (data, profile))
       continue;
 
     g_variant_builder_init (&asv_builder, G_VARIANT_TYPE ("a{sv}"));
     g_variant_builder_add (&asv_builder, "{sv}", "Profile",
-                           g_variant_new_string (ppd_profile_to_str (1 << i)));
-    g_variant_builder_add (&asv_builder, "{sv}", "Driver",
-                           g_variant_new_string (ppd_driver_get_driver_name (driver)));
+                           g_variant_new_string (ppd_profile_to_str (profile)));
+    cpu = driver_profile_support (cpu_driver, profile);
+    platform = driver_profile_support (platform_driver, profile);
+    if (cpu)
+      g_variant_builder_add (&asv_builder, "{sv}", "CpuDriver",
+                             g_variant_new_string (ppd_driver_get_driver_name (cpu_driver)));
+    if (platform)
+      g_variant_builder_add (&asv_builder, "{sv}", "PlatformDriver",
+                             g_variant_new_string (ppd_driver_get_driver_name (platform_driver)));
+
+    /* compatibility with older API */
+    if (cpu && platform)
+      driver = "multiple";
+    else if (cpu)
+      driver = ppd_driver_get_driver_name (cpu_driver);
+    else if (platform)
+      driver = ppd_driver_get_driver_name (platform_driver);
+
+    if (driver)
+      g_variant_builder_add (&asv_builder, "{sv}", "Driver",
+                             g_variant_new_string (driver));
 
     g_variant_builder_add (&builder, "a{sv}", &asv_builder);
   }
@@ -207,8 +276,10 @@ get_profile_holds_variant (PpdApp *data)
 }
 
 static void
-send_dbus_event (PpdApp     *data,
-                 PropertiesMask  mask)
+send_dbus_event_iface (PpdApp         *data,
+                       PropertiesMask  mask,
+                       const gchar    *iface,
+                       const gchar    *path)
 {
   GVariantBuilder props_builder;
   GVariant *props_changed = NULL;
@@ -231,8 +302,9 @@ send_dbus_event (PpdApp     *data,
                            g_variant_new_string (""));
   }
   if (mask & PROP_DEGRADED) {
+    gchar *degraded = get_performance_degraded (data);
     g_variant_builder_add (&props_builder, "{sv}", "PerformanceDegraded",
-                           g_variant_new_string (get_performance_degraded (data)));
+                           g_variant_new_take_string (g_steal_pointer (&degraded)));
   }
   if (mask & PROP_PROFILES) {
     g_variant_builder_add (&props_builder, "{sv}", "Profiles",
@@ -246,26 +318,53 @@ send_dbus_event (PpdApp     *data,
     g_variant_builder_add (&props_builder, "{sv}", "ActiveProfileHolds",
                            get_profile_holds_variant (data));
   }
+  if (mask & PROP_VERSION) {
+    g_variant_builder_add (&props_builder, "{sv}", "Version",
+                           g_variant_new_string (VERSION));
+  }
 
-  props_changed = g_variant_new ("(s at a{sv}@as)", POWER_PROFILES_IFACE_NAME,
+  props_changed = g_variant_new ("(s at a{sv}@as)", iface,
                                  g_variant_builder_end (&props_builder),
                                  g_variant_new_strv (NULL, 0));
 
   g_dbus_connection_emit_signal (data->connection,
                                  NULL,
-                                 POWER_PROFILES_DBUS_PATH,
+                                 path,
                                  "org.freedesktop.DBus.Properties",
                                  "PropertiesChanged",
                                  props_changed, NULL);
 }
 
+static void
+send_dbus_event (PpdApp         *data,
+                 PropertiesMask  mask)
+{
+  send_dbus_event_iface (data, mask,
+                         POWER_PROFILES_IFACE_NAME,
+                         POWER_PROFILES_DBUS_PATH);
+  send_dbus_event_iface (data, mask,
+                         POWER_PROFILES_LEGACY_IFACE_NAME,
+                         POWER_PROFILES_LEGACY_DBUS_PATH);
+}
+
 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 (PPD_IS_DRIVER_CPU (data->cpu_driver)) {
+    g_key_file_set_string (data->config, "State", "CpuDriver",
+                           ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)));
+  }
+
+  if (PPD_IS_DRIVER_PLATFORM (data->platform_driver)) {
+    g_key_file_set_string (data->config, "State", "PlatformDriver",
+                           ppd_driver_get_driver_name (PPD_DRIVER (data->platform_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);
 }
@@ -273,16 +372,26 @@ save_configuration (PpdApp *data)
 static gboolean
 apply_configuration (PpdApp *data)
 {
-  g_autofree char *driver = NULL;
+  g_autofree char *platform_driver = NULL;
   g_autofree char *profile_str = NULL;
+  g_autofree char *cpu_driver = 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)
+  cpu_driver = g_key_file_get_string (data->config, "State", "CpuDriver", NULL);
+  if (PPD_IS_DRIVER_CPU (data->cpu_driver) &&
+      g_strcmp0 (ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)), cpu_driver) != 0)
+    return FALSE;
+
+  platform_driver = g_key_file_get_string (data->config, "State", "PlatformDriver", NULL);
+
+  if (PPD_IS_DRIVER_PLATFORM (data->platform_driver) &&
+      g_strcmp0 (ppd_driver_get_driver_name (PPD_DRIVER (data->platform_driver)), platform_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);
@@ -339,18 +448,55 @@ activate_target_profile (PpdApp                      *data,
                          PpdProfileActivationReason   reason,
                          GError                     **error)
 {
-  GError *internal_error = NULL;
+  PpdProfile current_profile = data->active_profile;
+  gboolean cpu_set = TRUE;
+  gboolean platform_set = TRUE;
 
   g_debug ("Setting active profile '%s' for reason '%s' (current: '%s')",
            ppd_profile_to_str (target_profile),
            ppd_profile_activation_reason_to_str (reason),
-           ppd_profile_to_str (data->active_profile));
+           ppd_profile_to_str (current_profile));
+
+  /* Try CPU first */
+  if (driver_profile_support (PPD_DRIVER (data->cpu_driver), target_profile))
+    cpu_set = ppd_driver_activate_profile (PPD_DRIVER (data->cpu_driver),
+                                           target_profile, reason, error);
+
+  if (!cpu_set) {
+    g_prefix_error (error, "Failed to activate CPU driver '%s': ",
+                    ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)));
+    return FALSE;
+  }
+
+  /* Then try platform */
+  if (driver_profile_support (PPD_DRIVER (data->platform_driver), target_profile)) {
+    platform_set = ppd_driver_activate_profile (PPD_DRIVER (data->platform_driver),
+                                                target_profile, reason, error);
+  }
+
+  if (!platform_set) {
+    g_prefix_error (error, "Failed to activate platform driver '%s': ",
+                    ppd_driver_get_driver_name (PPD_DRIVER (data->platform_driver)));
+
+    /* Try to recover */
+    if (cpu_set) {
+      g_autoptr(GError) recovery_error = NULL;
+
+      g_debug ("Reverting CPU driver '%s' to profile '%s'",
+               ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)),
+               ppd_profile_to_str (current_profile));
+
+      if (!ppd_driver_activate_profile (PPD_DRIVER (data->cpu_driver),
+                                        current_profile, PPD_PROFILE_ACTIVATION_REASON_INTERNAL,
+                                        &recovery_error)) {
+        g_prefix_error (error, "Failed to revert CPU driver '%s': ",
+                        ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)));
+        g_warning ("Failed to revert CPU driver '%s': %s",
+                   ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)),
+                   recovery_error->message);
+      }
+    }
 
-  if (!ppd_driver_activate_profile (data->driver, target_profile, reason, &internal_error)) {
-    g_warning ("Failed to activate driver '%s': %s",
-               ppd_driver_get_driver_name (data->driver),
-               internal_error->message);
-    g_propagate_error (error, internal_error);
     return FALSE;
   }
 
@@ -365,6 +511,21 @@ activate_target_profile (PpdApp                      *data,
   return TRUE;
 }
 
+static void
+release_hold_notify (PpdApp      *data,
+                     ProfileHold *hold,
+                     guint        cookie)
+{
+  const char *req_path = POWER_PROFILES_DBUS_PATH;
+
+  if (g_strcmp0 (hold->requester_iface, POWER_PROFILES_LEGACY_IFACE_NAME) == 0)
+    req_path = POWER_PROFILES_LEGACY_DBUS_PATH;
+
+  g_dbus_connection_emit_signal (data->connection, hold->requester, req_path,
+                                 hold->requester_iface, "ProfileReleased",
+                                 g_variant_new ("(u)", cookie), NULL);
+}
+
 static void
 release_all_profile_holds (PpdApp *data)
 {
@@ -376,9 +537,7 @@ release_all_profile_holds (PpdApp *data)
     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);
+    release_hold_notify (data, hold, cookie);
     g_bus_unwatch_name (cookie);
   }
   g_hash_table_remove_all (data->profile_holds);
@@ -496,12 +655,13 @@ release_profile_hold (PpdApp *data,
 
   hold = g_hash_table_lookup (data->profile_holds, GUINT_TO_POINTER (cookie));
   if (!hold) {
-    g_debug("No hold with cookie %d", cookie);
+    g_debug ("No hold with cookie %d", cookie);
     return;
   }
 
   g_bus_unwatch_name (cookie);
   hold_profile = hold->profile;
+  release_hold_notify (data, hold, cookie);
   g_hash_table_remove (data->profile_holds, GUINT_TO_POINTER (cookie));
 
   if (g_hash_table_size (data->profile_holds) == 0 &&
@@ -587,8 +747,9 @@ hold_profile (PpdApp                *data,
   hold->reason = g_strdup (reason);
   hold->application_id = g_strdup (application_id);
   hold->requester = g_strdup (g_dbus_method_invocation_get_sender (invocation));
+  hold->requester_iface = g_strdup (g_dbus_method_invocation_get_interface_name (invocation));
 
-  g_debug ("%s(%s) requesting to hold profile '%s', reason: '%s'", application_id,
+  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,
@@ -676,10 +837,14 @@ handle_get_property (GDBusConnection *connection,
     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, "PerformanceDegraded") == 0) {
+    gchar *degraded = get_performance_degraded (data);
+    return g_variant_new_take_string (g_steal_pointer (&degraded));
+  }
   if (g_strcmp0 (property_name, "ActiveProfileHolds") == 0)
     return get_profile_holds_variant (data);
+  if (g_strcmp0 (property_name, "Version") == 0)
+    return g_variant_new_string (VERSION);
   return NULL;
 }
 
@@ -703,7 +868,9 @@ 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))
+  if (!check_action_permission (data, sender,
+                                POWER_PROFILES_POLICY_NAMESPACE ".switch-profile",
+                                error))
     return FALSE;
 
   g_variant_get (value, "&s", &profile);
@@ -723,7 +890,8 @@ handle_method_call (GDBusConnection       *connection,
   PpdApp *data = user_data;
   g_assert (data->connection);
 
-  if (g_strcmp0 (interface_name, POWER_PROFILES_IFACE_NAME) != 0) {
+  if (!g_str_equal (interface_name, POWER_PROFILES_IFACE_NAME) &&
+      !g_str_equal (interface_name, POWER_PROFILES_LEGACY_IFACE_NAME)) {
     g_dbus_method_invocation_return_error (invocation,G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                            "Unknown interface %s", interface_name);
     return;
@@ -733,7 +901,7 @@ handle_method_call (GDBusConnection       *connection,
     g_autoptr(GError) local_error = NULL;
     if (!check_action_permission (data,
                                   g_dbus_method_invocation_get_sender (invocation),
-                                  "net.hadess.PowerProfiles.hold-profile",
+                                  POWER_PROFILES_POLICY_NAMESPACE ".hold-profile",
                                   &local_error)) {
       g_dbus_method_invocation_return_gerror (invocation, local_error);
       return;
@@ -756,16 +924,42 @@ static const GDBusInterfaceVTable interface_vtable =
   handle_set_property
 };
 
+typedef struct {
+  PpdApp *app;
+  GBusNameOwnerFlags flags;
+  GDBusInterfaceInfo *interface;
+  GDBusInterfaceInfo *legacy_interface;
+} PpdBusOwnData;
+
+static void
+ppd_bus_own_data_free (PpdBusOwnData *data)
+{
+  g_clear_pointer (&data->interface, g_dbus_interface_info_unref);
+  g_clear_pointer (&data->legacy_interface, g_dbus_interface_info_unref);
+  g_free (data);
+}
+
 static void
 name_lost_handler (GDBusConnection *connection,
                    const gchar     *name,
                    gpointer         user_data)
 {
-  PpdApp *data = user_data;
+  PpdBusOwnData *data = user_data;
+  PpdApp *app = data->app;
+
   g_debug ("power-profiles-daemon is already running, or it cannot own its D-Bus name. Verify installation.");
-  if (!data->was_started)
-    data->ret = 1;
-  g_main_loop_quit (data->main_loop);
+  if (!app->was_started)
+    app->ret = 1;
+
+  g_main_loop_quit (app->main_loop);
+}
+
+static void
+legacy_name_acquired_handler (GDBusConnection *connection,
+                              const gchar     *name,
+                              gpointer         user_data)
+{
+  g_debug ("Name '%s' acquired", name);
 }
 
 static void
@@ -773,29 +967,43 @@ bus_acquired_handler (GDBusConnection *connection,
                       const gchar     *name,
                       gpointer         user_data)
 {
-  PpdApp *data = user_data;
+  PpdBusOwnData *data = user_data;
 
   g_dbus_connection_register_object (connection,
                                      POWER_PROFILES_DBUS_PATH,
-                                     data->introspection_data->interfaces[0],
+                                     data->interface,
                                      &interface_vtable,
-                                     data,
+                                     data->app,
                                      NULL,
                                      NULL);
 
-  data->connection = g_object_ref (connection);
+  g_dbus_connection_register_object (connection,
+                                     POWER_PROFILES_LEGACY_DBUS_PATH,
+                                     data->legacy_interface,
+                                     &interface_vtable,
+                                     data->app,
+                                     NULL,
+                                     NULL);
+
+  data->app->legacy_name_id = g_bus_own_name_on_connection (connection,
+                                                            POWER_PROFILES_LEGACY_DBUS_NAME,
+                                                            data->flags,
+                                                            legacy_name_acquired_handler,
+                                                            name_lost_handler,
+                                                            data,
+                                                            NULL);
+
+  data->app->connection = g_object_ref (connection);
 }
 
 static gboolean
 has_required_drivers (PpdApp *data)
 {
-  PpdDriver *driver;
-
-  driver = GET_DRIVER (PPD_PROFILE_BALANCED);
-  if (!driver || !G_IS_OBJECT (driver))
+  if (!PPD_IS_DRIVER_CPU (data->cpu_driver) &&
+      !PPD_IS_DRIVER_PLATFORM (data->platform_driver))
     return FALSE;
-  driver = GET_DRIVER (PPD_PROFILE_POWER_SAVER);
-  if (!driver || !G_IS_OBJECT (driver))
+
+  if (!get_profile_available (data, PPD_PROFILE_BALANCED | PPD_PROFILE_POWER_SAVER))
     return FALSE;
 
   return TRUE;
@@ -817,28 +1025,71 @@ 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);
+  g_clear_object (&data->cpu_driver);
+  g_clear_object (&data->platform_driver);
+}
+
+static gboolean
+action_blocked (PpdAction *action)
+{
+  const gchar *action_name = ppd_action_get_action_name (action);
+  const gchar *env = g_getenv ("POWER_PROFILE_DAEMON_ACTION_BLOCK");
+
+  if (env == NULL)
+    return FALSE;
+
+  g_auto(GStrv) actions = NULL;
+
+  actions = g_strsplit (env, ",", -1);
+  return g_strv_contains ((const gchar *const *)actions, action_name);
+}
+
+static gboolean
+driver_blocked (PpdDriver *driver)
+{
+  const gchar *driver_name = ppd_driver_get_driver_name (driver);
+  const gchar *env = g_getenv ("POWER_PROFILE_DAEMON_DRIVER_BLOCK");
+
+  if (env == NULL)
+    return FALSE;
+
+  g_auto(GStrv) drivers = NULL;
+  drivers = g_strsplit (env, ",", -1);
+  return g_strv_contains ((const char **) drivers, driver_name);
 }
 
 static void
 start_profile_drivers (PpdApp *data)
 {
   guint i;
+  g_autoptr(GError) initial_error = NULL;
 
   for (i = 0; i < G_N_ELEMENTS (objects); i++) {
-    GObject *object;
+    g_autoptr(GObject) object = NULL;
+
+    object = g_object_new (objects[i] (), NULL);
 
-    object = g_object_new (objects[i](), NULL);
     if (PPD_IS_DRIVER (object)) {
-      PpdDriver *driver = PPD_DRIVER (object);
+      g_autoptr(PpdDriver) driver = PPD_DRIVER (g_steal_pointer (&object));
       PpdProfile profiles;
       PpdProbeResult result;
 
       g_debug ("Handling driver '%s'", ppd_driver_get_driver_name (driver));
+      if (driver_blocked (driver)) {
+        g_debug ("Driver '%s' is blocked, skipping", ppd_driver_get_driver_name (driver));
+        continue;
+      }
+
+      if (PPD_IS_DRIVER_CPU (data->cpu_driver) && PPD_IS_DRIVER_CPU (driver)) {
+        g_debug ("CPU driver '%s' already probed, skipping driver '%s'",
+                 ppd_driver_get_driver_name (PPD_DRIVER (data->cpu_driver)),
+                 ppd_driver_get_driver_name (driver));
+        continue;
+      }
 
-      if (data->driver != NULL) {
-        g_debug ("Driver '%s' already probed, skipping driver '%s'",
-                 ppd_driver_get_driver_name (data->driver),
+      if (PPD_IS_DRIVER_PLATFORM (data->platform_driver) && PPD_IS_DRIVER_PLATFORM (driver)) {
+        g_debug ("Platform driver '%s' already probed, skipping driver '%s'",
+                 ppd_driver_get_driver_name (PPD_DRIVER (data->platform_driver)),
                  ppd_driver_get_driver_name (driver));
         continue;
       }
@@ -848,45 +1099,58 @@ start_profile_drivers (PpdApp *data)
         g_warning ("Profile Driver '%s' implements invalid profiles '0x%X'",
                    ppd_driver_get_driver_name (driver),
                    profiles);
-        g_object_unref (object);
         continue;
       }
 
       result = ppd_driver_probe (driver);
       if (result == PPD_PROBE_RESULT_FAIL) {
-        g_debug ("probe() failed for driver %s, skipping",
+        g_debug ("probe () failed for driver %s, skipping",
                  ppd_driver_get_driver_name (driver));
-        g_object_unref (object);
         continue;
-      } else if (result == PPD_PROBE_RESULT_DEFER) {
+      }
+
+      if (result == PPD_PROBE_RESULT_DEFER) {
         g_signal_connect (G_OBJECT (driver), "probe-request",
                           G_CALLBACK (driver_probe_request_cb), data);
-        g_ptr_array_add (data->probed_drivers, driver);
+        g_ptr_array_add (data->probed_drivers, g_steal_pointer (&driver));
         continue;
       }
 
-      data->driver = driver;
+      if (PPD_IS_DRIVER_CPU (driver))
+        g_set_object (&data->cpu_driver, PPD_DRIVER_CPU (driver));
+      else if (PPD_IS_DRIVER_PLATFORM (driver))
+        g_set_object (&data->platform_driver, PPD_DRIVER_PLATFORM (driver));
+      else
+        g_assert_not_reached ();
 
       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)) {
-      PpdAction *action = PPD_ACTION (object);
+      continue;
+    }
+
+    if (PPD_IS_ACTION (object)) {
+      g_autoptr(PpdAction) action = PPD_ACTION (g_steal_pointer (&object));
 
       g_debug ("Handling action '%s'", ppd_action_get_action_name (action));
 
-      if (!ppd_action_probe (action)) {
-        g_debug ("probe() failed for action '%s', skipping",
+      if (action_blocked (action)) {
+        g_debug ("Action '%s' is blocked, skipping", ppd_action_get_action_name (action));
+        continue;
+      }
+
+      if (ppd_action_probe (action) == PPD_PROBE_RESULT_FAIL) {
+        g_debug ("probe () failed for action '%s', skipping",
                  ppd_action_get_action_name (action));
-        g_object_unref (object);
         continue;
       }
 
-      g_ptr_array_add (data->actions, action);
-    } else {
-      g_assert_not_reached ();
+      g_ptr_array_add (data->actions, g_steal_pointer (&action));
+      continue;
     }
+
+    g_assert_not_reached ();
   }
 
   if (!has_required_drivers (data)) {
@@ -896,7 +1160,8 @@ start_profile_drivers (PpdApp *data)
 
   /* 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, NULL);
+  if (!activate_target_profile (data, data->active_profile, PPD_PROFILE_ACTIVATION_REASON_RESET, &initial_error))
+    g_warning ("Failed to activate initial profile: %s", initial_error->message);
 
   send_dbus_event (data, PROP_ALL);
 
@@ -922,37 +1187,65 @@ name_acquired_handler (GDBusConnection *connection,
                        const gchar     *name,
                        gpointer         user_data)
 {
-  PpdApp *data = user_data;
+  PpdBusOwnData *data = user_data;
 
-  start_profile_drivers (data);
+  g_debug ("Name '%s' acquired", name);
+
+  start_profile_drivers (data->app);
 }
 
 static gboolean
-setup_dbus (PpdApp   *data,
-            gboolean  replace)
+setup_dbus (PpdApp    *data,
+            gboolean   replace,
+            GError   **error)
 {
-  GBytes *bytes;
-  GBusNameOwnerFlags flags;
+  g_autoptr(GBytes) iface_data = NULL;
+  g_autoptr(GBytes) legacy_iface_data = NULL;
+  g_autoptr(GDBusNodeInfo) introspection_data = NULL;
+  g_autoptr(GDBusNodeInfo) legacy_introspection_data = NULL;
+  PpdBusOwnData *own_data;
+
+  iface_data = g_resources_lookup_data (POWER_PROFILES_RESOURCES_PATH "/"
+                                        POWER_PROFILES_DBUS_NAME ".xml",
+                                        G_RESOURCE_LOOKUP_FLAGS_NONE,
+                                        error);
+  if (!iface_data)
+    return FALSE;
+
+  legacy_iface_data = g_resources_lookup_data (POWER_PROFILES_RESOURCES_PATH "/"
+                                               POWER_PROFILES_LEGACY_DBUS_NAME ".xml",
+                                               G_RESOURCE_LOOKUP_FLAGS_NONE,
+                                               error);
+  if (!legacy_iface_data)
+    return FALSE;
+
+  introspection_data =  g_dbus_node_info_new_for_xml (g_bytes_get_data (iface_data, NULL),
+                                                      error);
+  if (!introspection_data)
+    return FALSE;
 
-  bytes = g_resources_lookup_data ("/net/hadess/PowerProfiles/net.hadess.PowerProfiles.xml",
-                                   G_RESOURCE_LOOKUP_FLAGS_NONE,
-                                   NULL);
-  data->introspection_data = g_dbus_node_info_new_for_xml (g_bytes_get_data (bytes, NULL), NULL);
-  g_bytes_unref (bytes);
-  g_assert (data->introspection_data != NULL);
+  legacy_introspection_data =  g_dbus_node_info_new_for_xml (g_bytes_get_data (legacy_iface_data, NULL),
+                                                             error);
+  if (!legacy_introspection_data)
+    return FALSE;
+
+  own_data = g_new0 (PpdBusOwnData, 1);
+  own_data->app = data;
+  own_data->interface = g_dbus_interface_info_ref (introspection_data->interfaces[0]);
+  own_data->legacy_interface = g_dbus_interface_info_ref (legacy_introspection_data->interfaces[0]);
 
-  flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
+  own_data->flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
   if (replace)
-    flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;
+    own_data->flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;
 
   data->name_id = g_bus_own_name (G_BUS_TYPE_SYSTEM,
                                   POWER_PROFILES_DBUS_NAME,
-                                  flags,
+                                  own_data->flags,
                                   bus_acquired_handler,
                                   name_acquired_handler,
                                   name_lost_handler,
-                                  data,
-                                  NULL);
+                                  own_data,
+                                  (GDestroyNotify) ppd_bus_own_data_free);
 
   return TRUE;
 }
@@ -963,37 +1256,92 @@ free_app_data (PpdApp *data)
   if (data == NULL)
     return;
 
-  if (data->name_id != 0) {
-    g_bus_unown_name (data->name_id);
-    data->name_id = 0;
-  }
+  g_clear_handle_id (&data->name_id, g_bus_unown_name);
+  g_clear_handle_id (&data->legacy_name_id, g_bus_unown_name);
 
   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_clear_object (&data->cpu_driver);
+  g_clear_object (&data->platform_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);
   g_clear_object (&data->connection);
   g_free (data);
   ppd_app = NULL;
 }
 
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (PpdApp, free_app_data)
+
 void
 main_loop_quit (void)
 {
   g_main_loop_quit (ppd_app->main_loop);
 }
 
+static inline gboolean
+use_colored_ouput (void)
+{
+  if (g_getenv ("NO_COLOR"))
+    return FALSE;
+  return isatty (fileno (stdout));
+}
+
+static void
+debug_handler_cb (const gchar *log_domain,
+                  GLogLevelFlags log_level,
+                  const gchar *message,
+                  gpointer user_data)
+{
+  PpdApp *data = user_data;
+  g_autoptr(GString) domain = NULL;
+  gboolean use_color;
+  gint color;
+
+  /* not in verbose mode */
+  if (log_level > data->log_level)
+   return;
+
+  domain = g_string_new (log_domain);
+  use_color = use_colored_ouput ();
+
+  for (gsize i = domain->len; i < 15; i++)
+    g_string_append_c (domain, ' ');
+  g_print ("%s", domain->str);
+
+  switch (log_level) {
+  case G_LOG_LEVEL_ERROR:
+  case G_LOG_LEVEL_CRITICAL:
+  case G_LOG_LEVEL_WARNING:
+    color = 31; /* red */
+    break;
+  default:
+    color = 34; /* blue */
+    break;
+  }
+
+  if (use_color)
+    g_print ("%c[%dm%s\n%c[%dm", 0x1B, color, message, 0x1B, 0);
+  else
+    g_print ("%s\n", message);
+}
+
+static gboolean
+quit_signal_callback (gpointer user_data)
+{
+  PpdApp *data = user_data;
+
+  g_main_loop_quit (data->main_loop);
+  return FALSE;
+}
+
 int main (int argc, char **argv)
 {
-  PpdApp *data;
-  int ret = 0;
+  g_autoptr (PpdApp) data = NULL;
   g_autoptr(GOptionContext) option_context = NULL;
   g_autoptr(GError) error = NULL;
   gboolean verbose = FALSE;
@@ -1008,17 +1356,11 @@ int main (int argc, char **argv)
   option_context = g_option_context_new ("");
   g_option_context_add_main_entries (option_context, options, NULL);
 
-  ret = g_option_context_parse (option_context, &argc, &argv, &error);
-  if (!ret) {
+  if (!g_option_context_parse (option_context, &argc, &argv, &error)) {
     g_print ("Failed to parse arguments: %s\n", error->message);
     return EXIT_FAILURE;
   }
 
-  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);
@@ -1027,15 +1369,27 @@ int main (int argc, char **argv)
   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;
+  data->log_level = verbose ? G_LOG_LEVEL_DEBUG : G_LOG_LEVEL_MESSAGE;
+
+  g_unix_signal_add (SIGTERM, quit_signal_callback, data);
+  g_unix_signal_add (SIGINT, quit_signal_callback, data);
+
+  /* redirect all domains */
+  if (verbose && !g_log_writer_is_journald (fileno (stdout)))
+    g_log_set_default_handler (debug_handler_cb, data);
+
+  g_debug ("Starting power-profiles-daemon version "VERSION);
+
   load_configuration (data);
   ppd_app = data;
 
   /* Set up D-Bus */
-  setup_dbus (data, replace);
+  if (!setup_dbus (data, replace, &error)) {
+    g_error ("Failed to start dbus: %s", error->message);
+    return 1;
+  }
 
   g_main_loop_run (data->main_loop);
-  ret = data->ret;
-  free_app_data (data);
 
-  return ret;
+  return data->ret;
 }
diff --git a/src/net.hadess.PowerProfiles.xml b/src/power-profiles-daemon.dbus.xml.in
similarity index 95%
rename from src/net.hadess.PowerProfiles.xml
rename to src/power-profiles-daemon.dbus.xml.in
index fcfc1d4..04cfa72 100644
--- a/src/net.hadess.PowerProfiles.xml
+++ b/src/power-profiles-daemon.dbus.xml.in
@@ -4,7 +4,7 @@
 <node>
 
   <!--
-      net.hadess.PowerProfiles:
+      @dbus_iface@:
       @short_description: Power Profiles daemon
 
       The power-profiles-daemon API is meant to be used by parts of the OS or
@@ -21,9 +21,9 @@
       existing projects is explained <ulink href=" https://gitlab.freedesktop.org/hadess/power-profiles-daemon/-/blob/master/README.md">
       in the project's README file</ulink>.
 
-      The object path will be "/net/hadess/PowerProfiles".
+      The object path will be "@dbus_path@".
   -->
-  <interface name="net.hadess.PowerProfiles">
+  <interface name="@dbus_iface@">
 
     <!--
         HoldProfile:
@@ -136,5 +136,12 @@
     -->
     <property name="ActiveProfileHolds" type="aa{sv}" access="read"/>
 
+    <!--
+        Version:
+
+        The version of the power-profiles-daemon software.
+    -->
+    <property name="Version" type="s" access="read"/>
+
   </interface>
 </node>
diff --git a/src/power-profiles-daemon.gresource.xml b/src/power-profiles-daemon.gresource.xml
deleted file mode 100644
index ae1ee65..0000000
--- a/src/power-profiles-daemon.gresource.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<gresources>
-	<gresource prefix="/net/hadess/PowerProfiles">
-		<file preprocess="xml-stripblanks">net.hadess.PowerProfiles.xml</file>
-	</gresource>
-</gresources>
-
diff --git a/src/power-profiles-daemon.gresource.xml.in b/src/power-profiles-daemon.gresource.xml.in
new file mode 100644
index 0000000..aaa90f9
--- /dev/null
+++ b/src/power-profiles-daemon.gresource.xml.in
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+	<gresource prefix="@prefix@">
+		@contents@
+	</gresource>
+</gresources>
diff --git a/src/powerprofilesctl b/src/powerprofilesctl
new file mode 100755
index 0000000..af4dfe1
--- /dev/null
+++ b/src/powerprofilesctl
@@ -0,0 +1,227 @@
+#!/usr/bin/env python3
+
+import argparse
+import signal
+import subprocess
+import sys
+from gi.repository import Gio, GLib
+
+PP_NAME = "org.freedesktop.UPower.PowerProfiles"
+PP_PATH = "/org/freedesktop/UPower/PowerProfiles"
+PP_IFACE = "org.freedesktop.UPower.PowerProfiles"
+PROPERTIES_IFACE = "org.freedesktop.DBus.Properties"
+
+
+def get_proxy():
+    try:
+        bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
+        proxy = Gio.DBusProxy.new_sync(
+            bus, Gio.DBusProxyFlags.NONE, None, PP_NAME, PP_PATH, PROPERTIES_IFACE, None
+        )
+    except:
+        raise
+    return proxy
+
+
+def command(func):
+    def wrapper(*args, **kwargs):
+        try:
+            func(*args, **kwargs)
+        except GLib.Error as error:
+            sys.stderr.write(
+                f"Failed to communicate with power-profiles-daemon: {error}\n"
+            )
+            sys.exit(1)
+        except ValueError as error:
+            sys.stderr.write(f"Error: {error}\n")
+            sys.exit(1)
+
+    return wrapper
+
+
+ at command
+def _version(args):  # pylint: disable=unused-argument
+    proxy = get_proxy()
+    ver = proxy.Get("(ss)", PP_IFACE, "Version")
+    print(ver)
+
+
+ at command
+def _get(args):  # pylint: disable=unused-argument
+    proxy = get_proxy()
+    profile = proxy.Get("(ss)", PP_IFACE, "ActiveProfile")
+    print(profile)
+
+
+ at command
+def _set(args):
+    try:
+        proxy = get_proxy()
+        proxy.Set(
+            "(ssv)", PP_IFACE, "ActiveProfile", GLib.Variant.new_string(args.profile[0])
+        )
+    except:
+        raise
+
+
+def get_profiles_property(prop):
+    try:
+        proxy = get_proxy()
+    except:
+        raise
+
+    profiles = None
+    try:
+        profiles = proxy.Get("(ss)", PP_IFACE, prop)
+    except:
+        raise
+    return profiles
+
+
+ at command
+def _list(args):  # pylint: disable=unused-argument
+    try:
+        profiles = get_profiles_property("Profiles")
+        reason = get_proxy().Get("(ss)", PP_IFACE, "PerformanceDegraded")
+        degraded = reason != ""
+        active = get_proxy().Get("(ss)", PP_IFACE, "ActiveProfile")
+    except:
+        raise
+
+    index = 0
+    for profile in reversed(profiles):
+        if index > 0:
+            print("")
+        marker = "*" if profile["Profile"] == active else " "
+        print(f'{marker} {profile["Profile"]}:')
+        for driver in ["CpuDriver", "PlatformDriver"]:
+            if driver not in profile:
+                continue
+            value = profile[driver]
+            print(f"    {driver}:\t{value}")
+        if profile["Profile"] == "performance":
+            print("    Degraded:  ", f"yes ({reason})" if degraded else "no")
+        index += 1
+
+
+ at command
+def _list_holds(args):  # pylint: disable=unused-argument
+    try:
+        holds = get_profiles_property("ActiveProfileHolds")
+    except:
+        raise
+
+    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
+
+
+ at command
+def _launch(args):
+    reason = args.reason
+    profile = args.profile
+    appid = args.appid
+    if not args.arguments:
+        raise ValueError("No command to launch")
+    if not args.appid:
+        appid = args.arguments[0]
+    if not profile:
+        profile = "performance"
+    if not reason:
+        reason = f"Running {args.appid}"
+    ret = 0
+    try:
+        bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
+        proxy = Gio.DBusProxy.new_sync(
+            bus, Gio.DBusProxyFlags.NONE, None, PP_NAME, PP_PATH, PP_IFACE, None
+        )
+    except:
+        raise
+
+    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.arguments) as launched_app:
+        try:
+            launched_app.wait()
+            ret = launched_app.returncode
+        except KeyboardInterrupt:
+            ret = launched_app.returncode
+    proxy.ReleaseProfile("(u)", cookie)
+
+    return ret
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        epilog="Use “powerprofilesctl COMMAND --help” to get detailed help for individual commands",
+    )
+    subparsers = parser.add_subparsers(help="Individual command help", dest="command")
+    parser_list = subparsers.add_parser("list", help="List available power profiles")
+    parser_list.set_defaults(func=_list)
+    parser_list_holds = subparsers.add_parser(
+        "list-holds", help="List current power profile holds"
+    )
+    parser_list_holds.set_defaults(func=_list_holds)
+    parser_get = subparsers.add_parser(
+        "get", help="Print the currently active power profile"
+    )
+    parser_get.set_defaults(func=_get)
+    parser_set = subparsers.add_parser(
+        "set", help="Set the currently active power profile"
+    )
+    parser_set.add_argument(
+        "profile",
+        nargs=1,
+        help="Profile to use for set command",
+    )
+    parser_set.set_defaults(func=_set)
+    parser_launch = subparsers.add_parser(
+        "launch",
+        help="Launch a command while holding a power profile",
+        description="Launch the command while holding a power profile,"
+        "either performance, or power-saver. By default, the profile hold "
+        "is for the performance profile, but it might not be available on "
+        "all systems. See the list command for a list of available profiles.",
+    )
+    parser_launch.add_argument(
+        "arguments",
+        nargs="*",
+        help="Command to launch",
+    )
+    parser_launch.add_argument(
+        "--profile", "-p", required=False, help="Profile to use for launch command"
+    )
+    parser_launch.add_argument(
+        "--reason", "-r", required=False, help="Reason to use for launch command"
+    )
+    parser_launch.add_argument(
+        "--appid", "-i", required=False, help="AppId to use for launch command"
+    )
+    parser_launch.set_defaults(func=_launch)
+    parser_version = subparsers.add_parser(
+        "version", help="Print version information and exit"
+    )
+    parser_version.set_defaults(func=_version)
+
+    args = parser.parse_args()
+    # default behavior is to run list if no command is given
+    if not args.command:
+        args.func = _list
+    args.func(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/powerprofilesctl.in b/src/powerprofilesctl.in
deleted file mode 100755
index b1ea675..0000000
--- a/src/powerprofilesctl.in
+++ /dev/null
@@ -1,282 +0,0 @@
-#!@PYTHON3@
-
-import signal
-import subprocess
-import sys
-from gi.repository import Gio, GLib
-
-VERSION = '@VERSION@'
-
-def usage_main():
-    print('Usage:')
-    print('  powerprofilesctl COMMAND [ARGS…]')
-    print('')
-    print('Commands:')
-    print('  help       Print help')
-    print('  version    Print version')
-    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.')
-
-def usage_version():
-    print('Usage:')
-    print('  powerprofilesctl version')
-    print('')
-    print('Print version information and exit.')
-
-def usage_get():
-    print('Usage:')
-    print('  powerprofilesctl get')
-    print('')
-    print('Print the currently active power profile.')
-
-def usage_set():
-    print('Usage:')
-    print('  powerprofilesctl set PROFILE')
-    print('')
-    print('Set the currently active power profile. Must be one of the ')
-    print('available profiles.')
-
-def usage_list():
-    print('Usage:')
-    print('  powerprofilesctl 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()
-    elif _command == 'get':
-        usage_get()
-    elif _command == 'set':
-        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:
-        usage_main()
-
-def version():
-    print (VERSION)
-
-def get_proxy():
-    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',
-                                       'org.freedesktop.DBus.Properties', None)
-    except:
-        raise
-    return proxy
-
-def _get():
-    proxy = get_proxy()
-    profile = proxy.Get('(ss)', 'net.hadess.PowerProfiles', 'ActiveProfile')
-    print(profile)
-
-def _set(profile):
-    try:
-        proxy = get_proxy()
-        proxy.Set('(ssv)',
-            'net.hadess.PowerProfiles',
-            'ActiveProfile',
-            GLib.Variant.new_string(profile))
-    except:
-        raise
-
-def get_profiles_property(prop):
-    try:
-        proxy = get_proxy()
-    except:
-        raise
-
-    profiles = None
-    try:
-        profiles = proxy.Get('(ss)', 'net.hadess.PowerProfiles', prop)
-    except:
-        raise
-    return profiles
-
-def _list():
-    try:
-        profiles = get_profiles_property('Profiles')
-        reason = get_proxy().Get('(ss)', 'net.hadess.PowerProfiles', 'PerformanceDegraded')
-        degraded = reason != ''
-        active = get_proxy().Get('(ss)', 'net.hadess.PowerProfiles', 'ActiveProfile')
-    except:
-        raise
-
-    index = 0
-    for profile in reversed(profiles):
-        if index > 0:
-            print('')
-        marker = '*' if profile['Profile'] == active else ' '
-        print(f'{marker} {profile["Profile"]}:')
-        print('    Driver:    ', profile['Driver'])
-        if profile['Profile'] == 'performance':
-            print('    Degraded:  ', f'yes ({reason})' if degraded else 'no')
-        index += 1
-
-def _list_holds():
-    try:
-        holds = get_profiles_property('ActiveProfileHolds')
-    except:
-        raise
-
-    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
-
-    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'
-    elif len(sys.argv) >= 2:
-        command = sys.argv[1]
-        if command == '--help':
-            command = 'help'
-        if command == '--version':
-            command = 'version'
-        else:
-            args = sys.argv[2:]
-
-    if command == 'help':
-        if len(args) > 0:
-            usage(args[0])
-        else:
-            usage(None)
-    elif command == 'version':
-        version()
-    elif command == 'get':
-        try:
-            _get()
-        except GLib.Error as error:
-            sys.stderr.write(f'Failed to communicate with power-profiles-daemon: {format(error)}\n')
-            sys.exit(1)
-    elif command == 'set':
-        if len(args) != 1:
-            usage_set()
-            sys.exit(1)
-        try:
-            _set(args[0])
-        except GLib.Error as error:
-            sys.stderr.write(f'Failed to communicate with power-profiles-daemon: {format(error)}\n')
-            sys.exit(1)
-    elif command == 'list':
-        try:
-            _list()
-        except GLib.Error as error:
-            sys.stderr.write(f'Failed to communicate with power-profiles-daemon: {format(error)}\n')
-            sys.exit(1)
-    elif command == 'list-holds':
-        try:
-            _list_holds()
-        except GLib.Error as error:
-            sys.stderr.write(f'Failed to communicate with power-profiles-daemon: {format(error)}\n')
-            sys.exit(1)
-    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'
-        try:
-            _launch(args, profile, appid, reason)
-        except GLib.Error as error:
-            sys.stderr.write(f'Failed to communicate with power-profiles-daemon: {format(error)}\n')
-            sys.exit(1)
-
-if __name__ == '__main__':
-    main()
diff --git a/src/ppd-action-amdgpu-panel-power.c b/src/ppd-action-amdgpu-panel-power.c
new file mode 100644
index 0000000..c2e824b
--- /dev/null
+++ b/src/ppd-action-amdgpu-panel-power.c
@@ -0,0 +1,350 @@
+/*
+ * Copyright (c) 2024 Advanced Micro Devices
+ *
+ * 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
+ * the Free Software Foundation.
+ *
+ */
+
+#define G_LOG_DOMAIN "AmdgpuAction"
+
+#include "config.h"
+
+#include <gudev/gudev.h>
+
+#include "ppd-action-amdgpu-panel-power.h"
+#include "ppd-profile.h"
+#include "ppd-utils.h"
+
+#define PROC_CPUINFO_PATH      "/proc/cpuinfo"
+
+#define PANEL_POWER_SYSFS_NAME "amdgpu/panel_power_savings"
+#define PANEL_STATUS_SYSFS_NAME "status"
+
+#define UPOWER_DBUS_NAME       "org.freedesktop.UPower"
+#define UPOWER_DBUS_PATH       "/org/freedesktop/UPower"
+#define UPOWER_DBUS_INTERFACE  "org.freedesktop.UPower"
+
+/**
+ * SECTION:ppd-action-amdgpu-panel-power
+ * @Short_description: Power savings for eDP connected displays
+ * @Title: AMDGPU Panel power action
+ *
+ * The AMDGPU panel power action utilizes the sysfs attribute present on some DRM
+ * connectors for amdgpu called "panel_power_savings".  This will use an AMD specific
+ * hardware feature for a power savings profile for the panel.
+ *
+ */
+
+struct _PpdActionAmdgpuPanelPower
+{
+  PpdAction  parent_instance;
+  PpdProfile last_profile;
+
+  GUdevClient *client;
+  GDBusProxy *proxy;
+  guint watcher_id;
+
+  gint panel_power_saving;
+  gboolean on_battery;
+};
+
+G_DEFINE_TYPE (PpdActionAmdgpuPanelPower, ppd_action_amdgpu_panel_power, PPD_TYPE_ACTION)
+
+static GObject*
+ppd_action_amdgpu_panel_power_constructor (GType                  type,
+                                           guint                  n_construct_params,
+                                           GObjectConstructParam *construct_params)
+{
+  GObject *object;
+
+  object = G_OBJECT_CLASS (ppd_action_amdgpu_panel_power_parent_class)->constructor (type,
+                                                                                     n_construct_params,
+                                                                                     construct_params);
+  g_object_set (object,
+                "action-name", "amdgpu_panel_power",
+                NULL);
+
+  return object;
+}
+
+static gboolean
+panel_connected (GUdevDevice *device)
+{
+  const char *value;
+  g_autofree gchar *stripped = NULL;
+
+  value = g_udev_device_get_sysfs_attr_uncached (device, PANEL_STATUS_SYSFS_NAME);
+  if (!value)
+    return FALSE;
+  stripped = g_strchomp (g_strdup (value));
+
+  return g_strcmp0 (stripped, "connected") == 0;
+}
+
+static gboolean
+set_panel_power (PpdActionAmdgpuPanelPower *self, gint power, GError **error)
+{
+  GList *devices, *l;
+
+  devices = g_udev_client_query_by_subsystem (self->client, "drm");
+  if (devices == NULL) {
+    g_set_error_literal (error,
+                         G_IO_ERROR,
+                         G_IO_ERROR_NOT_FOUND,
+                         "no drm devices found");
+    return FALSE;
+  }
+
+  for (l = devices; l != NULL; l = l->next) {
+    GUdevDevice *dev = l->data;
+    const char *value;
+    guint64 parsed;
+
+    value = g_udev_device_get_devtype (dev);
+    if (g_strcmp0 (value, "drm_connector") != 0)
+      continue;
+
+    if (!panel_connected (dev))
+      continue;
+
+    value = g_udev_device_get_sysfs_attr_uncached (dev, PANEL_POWER_SYSFS_NAME);
+    if (!value)
+      continue;
+
+    parsed = g_ascii_strtoull (value, NULL, 10);
+
+    /* overflow check */
+    if (parsed == G_MAXUINT64) {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "cannot parse %s as caused overflow",
+                   value);
+      return FALSE;
+    }
+
+    if (parsed == power)
+      continue;
+
+    if (!ppd_utils_write_sysfs_int (dev, PANEL_POWER_SYSFS_NAME, power, error))
+      return FALSE;
+
+    break;
+  }
+
+  g_list_free_full (devices, g_object_unref);
+
+  return TRUE;
+}
+
+static gboolean
+ppd_action_amdgpu_panel_update_target (PpdActionAmdgpuPanelPower  *self,
+                                       GError                    **error)
+{
+  gint target = 0;
+
+  /* only activate if we know that we're on battery */
+  if (self->on_battery) {
+    switch (self->last_profile) {
+    case PPD_PROFILE_POWER_SAVER:
+      target = 3;
+      break;
+    case PPD_PROFILE_BALANCED:
+      target = 1;
+      break;
+    case PPD_PROFILE_PERFORMANCE:
+      target = 0;
+      break;
+    }
+  }
+
+  if (!set_panel_power (self, target, error))
+    return FALSE;
+  self->panel_power_saving = target;
+
+  return TRUE;
+}
+
+static gboolean
+ppd_action_amdgpu_panel_power_activate_profile (PpdAction   *action,
+                                                PpdProfile   profile,
+                                                GError     **error)
+{
+  PpdActionAmdgpuPanelPower *self = PPD_ACTION_AMDGPU_PANEL_POWER (action);
+  self->last_profile = profile;
+
+  if (self->proxy == NULL) {
+    g_debug ("upower not available; battery data might be stale");
+    return TRUE;
+  }
+
+  return ppd_action_amdgpu_panel_update_target (self, error);
+}
+
+
+static void
+upower_properties_changed (GDBusProxy *proxy,
+                           GVariant *changed_properties,
+                           GStrv invalidated_properties,
+                           PpdActionAmdgpuPanelPower *self)
+{
+  g_autoptr (GVariant) battery_val = NULL;
+  g_autoptr (GError) error = NULL;
+  gboolean new_on_battery;
+
+  if (proxy != NULL)
+    battery_val = g_dbus_proxy_get_cached_property (proxy, "OnBattery");
+  new_on_battery = battery_val ? g_variant_get_boolean (battery_val) : FALSE;
+
+  if (self->on_battery == new_on_battery)
+    return;
+
+  g_debug ("OnBattery: %d -> %d", self->on_battery, new_on_battery);
+  self->on_battery = new_on_battery;
+  if (!ppd_action_amdgpu_panel_update_target (self, &error))
+    g_warning ("failed to update target: %s", error->message);
+}
+
+static void
+udev_uevent_cb (GUdevClient *client,
+                gchar       *action,
+                GUdevDevice *device,
+                gpointer     user_data)
+{
+  PpdActionAmdgpuPanelPower *self = user_data;
+
+  if (!g_str_equal (action, "add"))
+    return;
+
+  if (!g_udev_device_has_sysfs_attr (device, PANEL_POWER_SYSFS_NAME))
+    return;
+
+  if (!panel_connected (device))
+      return;
+
+  g_debug ("Updating panel power saving for '%s' to '%d'",
+           g_udev_device_get_sysfs_path (device),
+           self->panel_power_saving);
+  ppd_utils_write_sysfs_int (device, PANEL_POWER_SYSFS_NAME,
+                             self->panel_power_saving, NULL);
+}
+
+static PpdProbeResult
+ppd_action_amdgpu_panel_power_probe (PpdAction *action)
+{
+  g_autofree gchar *cpuinfo_path = NULL;
+  g_autofree gchar *cpuinfo = NULL;
+  g_auto(GStrv) lines = NULL;
+
+  cpuinfo_path = ppd_utils_get_sysfs_path (PROC_CPUINFO_PATH);
+  if (!g_file_get_contents (cpuinfo_path, &cpuinfo, NULL, NULL))
+    return PPD_PROBE_RESULT_FAIL;
+
+  lines = g_strsplit (cpuinfo, "\n", -1);
+
+  for (gchar **line = lines; *line != NULL; line++) {
+      if (g_str_has_prefix (*line, "vendor_id") &&
+          strchr (*line, ':')) {
+          g_auto(GStrv) sections = g_strsplit (*line, ":", 2);
+
+          if (g_strv_length (sections) < 2)
+            continue;
+          if (g_strcmp0 (g_strstrip (sections[1]), "AuthenticAMD") == 0)
+            return PPD_PROBE_RESULT_SUCCESS;
+      }
+  }
+
+
+  return PPD_PROBE_RESULT_FAIL;
+}
+
+static void
+upower_name_vanished (GDBusConnection *connection,
+                      const gchar     *name,
+                      gpointer         user_data)
+{
+  PpdActionAmdgpuPanelPower *self = user_data;
+
+  g_debug ("%s vanished", UPOWER_DBUS_NAME);
+
+  /* reset */
+  g_clear_pointer (&self->proxy, g_object_unref);
+  upower_properties_changed (NULL, NULL, NULL, self);
+}
+
+static void
+upower_name_appeared (GDBusConnection *connection,
+                      const gchar     *name,
+                      const gchar     *name_owner,
+                      gpointer         user_data)
+{
+  PpdActionAmdgpuPanelPower *self = user_data;
+  g_autoptr (GError) error = NULL;
+
+  g_debug ("%s appeared", UPOWER_DBUS_NAME);
+  self->proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                               G_DBUS_PROXY_FLAGS_NONE,
+                                               NULL,
+                                               UPOWER_DBUS_NAME,
+                                               UPOWER_DBUS_PATH,
+                                               UPOWER_DBUS_INTERFACE,
+                                               NULL,
+                                               &error);
+  if (self->proxy == NULL) {
+    g_debug ("failed to connect to upower: %s", error->message);
+    return;
+  }
+
+  g_signal_connect (self->proxy,
+                    "g-properties-changed",
+                    G_CALLBACK(upower_properties_changed),
+                    self);
+  upower_properties_changed (self->proxy, NULL, NULL, self);
+}
+
+static void
+ppd_action_amdgpu_panel_power_finalize (GObject *object)
+{
+  PpdActionAmdgpuPanelPower *action;
+
+  action = PPD_ACTION_AMDGPU_PANEL_POWER (object);
+  g_clear_handle_id (&action->watcher_id, g_bus_unwatch_name);
+  g_clear_object (&action->client);
+  g_clear_pointer (&action->proxy, g_object_unref);
+  G_OBJECT_CLASS (ppd_action_amdgpu_panel_power_parent_class)->finalize (object);
+}
+
+static void
+ppd_action_amdgpu_panel_power_class_init (PpdActionAmdgpuPanelPowerClass *klass)
+{
+  GObjectClass *object_class;
+  PpdActionClass *driver_class;
+
+  object_class = G_OBJECT_CLASS(klass);
+  object_class->constructor = ppd_action_amdgpu_panel_power_constructor;
+  object_class->finalize = ppd_action_amdgpu_panel_power_finalize;
+
+  driver_class = PPD_ACTION_CLASS(klass);
+  driver_class->probe = ppd_action_amdgpu_panel_power_probe;
+  driver_class->activate_profile = ppd_action_amdgpu_panel_power_activate_profile;
+}
+
+static void
+ppd_action_amdgpu_panel_power_init (PpdActionAmdgpuPanelPower *self)
+{
+  const gchar * const subsystem[] = { "drm", NULL };
+
+  self->client = g_udev_client_new (subsystem);
+  g_signal_connect (G_OBJECT (self->client), "uevent",
+                    G_CALLBACK (udev_uevent_cb), self);
+
+  self->watcher_id = g_bus_watch_name (G_BUS_TYPE_SYSTEM,
+                                       UPOWER_DBUS_NAME,
+                                       G_BUS_NAME_WATCHER_FLAGS_NONE,
+                                       upower_name_appeared,
+                                       upower_name_vanished,
+                                       self,
+                                       NULL);
+}
diff --git a/src/ppd-action-amdgpu-panel-power.h b/src/ppd-action-amdgpu-panel-power.h
new file mode 100644
index 0000000..9c0e797
--- /dev/null
+++ b/src/ppd-action-amdgpu-panel-power.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2024 Advanced Micro Devices
+ *
+ * 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
+ * the Free Software Foundation.
+ *
+ */
+
+#pragma once
+
+#include "ppd-action.h"
+
+#define PPD_TYPE_ACTION_AMDGPU_PANEL_POWER (ppd_action_amdgpu_panel_power_get_type())
+G_DECLARE_FINAL_TYPE(PpdActionAmdgpuPanelPower, ppd_action_amdgpu_panel_power, PPD, ACTION_AMDGPU_PANEL_POWER, PpdAction)
diff --git a/src/ppd-action-trickle-charge.c b/src/ppd-action-trickle-charge.c
index 629f21d..8347727 100644
--- a/src/ppd-action-trickle-charge.c
+++ b/src/ppd-action-trickle-charge.c
@@ -7,6 +7,8 @@
  *
  */
 
+#define G_LOG_DOMAIN "TrickleChargeAction"
+
 #include <gudev/gudev.h>
 
 #include "ppd-action-trickle-charge.h"
@@ -130,11 +132,11 @@ ppd_action_trickle_charge_class_init (PpdActionTrickleChargeClass *klass)
   GObjectClass *object_class;
   PpdActionClass *driver_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->constructor = ppd_action_trickle_charge_constructor;
   object_class->finalize = ppd_action_trickle_charge_finalize;
 
-  driver_class = PPD_ACTION_CLASS(klass);
+  driver_class = PPD_ACTION_CLASS (klass);
   driver_class->activate_profile = ppd_action_trickle_charge_activate_profile;
 }
 
diff --git a/src/ppd-action-trickle-charge.h b/src/ppd-action-trickle-charge.h
index 78ffd9b..e6b8850 100644
--- a/src/ppd-action-trickle-charge.h
+++ b/src/ppd-action-trickle-charge.h
@@ -11,5 +11,5 @@
 
 #include "ppd-action.h"
 
-#define PPD_TYPE_ACTION_TRICKLE_CHARGE (ppd_action_trickle_charge_get_type())
-G_DECLARE_FINAL_TYPE(PpdActionTrickleCharge, ppd_action_trickle_charge, PPD, ACTION_TRICKLE_CHARGE, PpdAction)
+#define PPD_TYPE_ACTION_TRICKLE_CHARGE (ppd_action_trickle_charge_get_type ())
+G_DECLARE_FINAL_TYPE (PpdActionTrickleCharge, ppd_action_trickle_charge, PPD, ACTION_TRICKLE_CHARGE, PpdAction)
diff --git a/src/ppd-action.c b/src/ppd-action.c
index 92ef128..e550bcc 100644
--- a/src/ppd-action.c
+++ b/src/ppd-action.c
@@ -7,6 +7,8 @@
  *
  */
 
+#define G_LOG_DOMAIN "Action"
+
 #include "ppd-action.h"
 #include "ppd-enums.h"
 
@@ -62,7 +64,7 @@ ppd_action_set_property (GObject        *object,
     priv->action_name = g_value_dup_string (value);
     break;
   default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
   }
 }
 
@@ -80,7 +82,7 @@ ppd_action_get_property (GObject        *object,
     g_value_set_string (value, priv->action_name);
     break;
   default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
   }
 }
 
@@ -100,7 +102,7 @@ ppd_action_class_init (PpdActionClass *klass)
 {
   GObjectClass *object_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->finalize = ppd_action_finalize;
   object_class->get_property = ppd_action_get_property;
   object_class->set_property = ppd_action_set_property;
@@ -111,7 +113,7 @@ ppd_action_class_init (PpdActionClass *klass)
    * A unique action name, only used for debugging.
    */
   g_object_class_install_property (object_class, PROP_ACTION_NAME,
-                                   g_param_spec_string("action-name",
+                                   g_param_spec_string ("action-name",
                                                        "Action name",
                                                        "Action name",
                                                        NULL,
@@ -123,13 +125,13 @@ ppd_action_init (PpdAction *self)
 {
 }
 
-gboolean
+PpdProbeResult
 ppd_action_probe (PpdAction *action)
 {
   g_return_val_if_fail (PPD_IS_ACTION (action), FALSE);
 
   if (!PPD_ACTION_GET_CLASS (action)->probe)
-    return TRUE;
+    return PPD_PROBE_RESULT_SUCCESS;
 
   return PPD_ACTION_GET_CLASS (action)->probe (action);
 }
diff --git a/src/ppd-action.h b/src/ppd-action.h
index dd5870a..9b74ce3 100644
--- a/src/ppd-action.h
+++ b/src/ppd-action.h
@@ -12,8 +12,8 @@
 #include <glib-object.h>
 #include "ppd-profile.h"
 
-#define PPD_TYPE_ACTION (ppd_action_get_type())
-G_DECLARE_DERIVABLE_TYPE(PpdAction, ppd_action, PPD, ACTION, GObject)
+#define PPD_TYPE_ACTION (ppd_action_get_type ())
+G_DECLARE_DERIVABLE_TYPE (PpdAction, ppd_action, PPD, ACTION, GObject)
 
 /**
  * PpdActionClass:
@@ -28,14 +28,14 @@ struct _PpdActionClass
 {
   GObjectClass   parent_class;
 
-  gboolean       (* probe)            (PpdAction  *action);
+  PpdProbeResult (* probe)            (PpdAction  *action);
   gboolean       (* activate_profile) (PpdAction  *action,
                                        PpdProfile  profile,
                                        GError    **error);
 };
 
 #ifndef __GTK_DOC_IGNORE__
-gboolean ppd_action_probe (PpdAction *action);
+PpdProbeResult ppd_action_probe (PpdAction *action);
 gboolean ppd_action_activate_profile (PpdAction *action, PpdProfile profile, GError **error);
 const char *ppd_action_get_action_name (PpdAction *action);
 #endif
diff --git a/src/ppd-driver-amd-pstate.c b/src/ppd-driver-amd-pstate.c
index 825caca..b1f6b11 100644
--- a/src/ppd-driver-amd-pstate.c
+++ b/src/ppd-driver-amd-pstate.c
@@ -8,24 +8,39 @@
  *
  */
 
+#define G_LOG_DOMAIN "CpuDriver"
+
 #include <upower.h>
 
 #include "ppd-utils.h"
 #include "ppd-driver-amd-pstate.h"
 
 #define CPUFREQ_POLICY_DIR "/sys/devices/system/cpu/cpufreq/"
-#define DEFAULT_CPU_FREQ_SCALING_GOV "powersave"
 #define PSTATE_STATUS_PATH "/sys/devices/system/cpu/amd_pstate/status"
+#define ACPI_PM_PROFILE "/sys/firmware/acpi/pm_profile"
+
+enum acpi_preferred_pm_profiles {
+  PM_UNSPECIFIED = 0,
+  PM_DESKTOP = 1,
+  PM_MOBILE = 2,
+  PM_WORKSTATION = 3,
+  PM_ENTERPRISE_SERVER = 4,
+  PM_SOHO_SERVER = 5,
+  PM_APPLIANCE_PC = 6,
+  PM_PERFORMANCE_SERVER = 7,
+  PM_TABLET = 8,
+  NR_PM_PROFILES = 9
+};
 
 struct _PpdDriverAmdPstate
 {
-  PpdDriver  parent_instance;
+  PpdDriverCpu  parent_instance;
 
   PpdProfile activated_profile;
   GList *epp_devices; /* GList of paths */
 };
 
-G_DEFINE_TYPE (PpdDriverAmdPstate, ppd_driver_amd_pstate, PPD_TYPE_DRIVER)
+G_DEFINE_TYPE (PpdDriverAmdPstate, ppd_driver_amd_pstate, PPD_TYPE_DRIVER_CPU)
 
 static gboolean ppd_driver_amd_pstate_activate_profile (PpdDriver                   *driver,
                                                         PpdProfile                   profile,
@@ -57,6 +72,9 @@ probe_epp (PpdDriverAmdPstate *pstate)
   g_autofree char *policy_dir = NULL;
   g_autofree char *pstate_status_path = NULL;
   g_autofree char *status = NULL;
+  g_autofree char *pm_profile_path = NULL;
+  g_autofree char *pm_profile_str = NULL;
+  guint64 pm_profile;
   const char *dirname;
   PpdProbeResult ret = PPD_PROBE_RESULT_FAIL;
 
@@ -77,29 +95,38 @@ probe_epp (PpdDriverAmdPstate *pstate)
     return ret;
   }
 
+  /* only run on things that we know aren't servers */
+  pm_profile_path = ppd_utils_get_sysfs_path (ACPI_PM_PROFILE);
+  if (!g_file_get_contents (pm_profile_path, &pm_profile_str, NULL, NULL))
+    return ret;
+  pm_profile = g_ascii_strtoull (pm_profile_str, NULL, 10);
+  switch (pm_profile) {
+  case PM_UNSPECIFIED:
+  case PM_ENTERPRISE_SERVER:
+  case PM_SOHO_SERVER:
+  case PM_PERFORMANCE_SERVER:
+    g_debug ("AMD-P-State not supported on PM profile %" G_GUINT64_FORMAT, pm_profile);
+    return ret;
+  default:
+    break;
+  }
+
   while ((dirname = g_dir_read_name (dir)) != NULL) {
+    g_autofree char *base = NULL;
     g_autofree char *path = NULL;
-    g_autofree char *gov_path = NULL;
     g_autoptr(GError) error = NULL;
 
-    path = g_build_filename (policy_dir,
+    base = g_build_filename (policy_dir,
                              dirname,
+                             NULL);
+
+    path = g_build_filename (base,
                              "energy_performance_preference",
                              NULL);
     if (!g_file_test (path, G_FILE_TEST_EXISTS))
       continue;
 
-    /* Force a scaling_governor where the preference can be written */
-    gov_path = g_build_filename (policy_dir,
-                                 dirname,
-                                 "scaling_governor",
-                                 NULL);
-    if (!ppd_utils_write (gov_path, DEFAULT_CPU_FREQ_SCALING_GOV, &error)) {
-      g_warning ("Could not change scaling governor %s to '%s'", dirname, DEFAULT_CPU_FREQ_SCALING_GOV);
-      continue;
-    }
-
-    pstate->epp_devices = g_list_prepend (pstate->epp_devices, g_steal_pointer (&path));
+    pstate->epp_devices = g_list_prepend (pstate->epp_devices, g_steal_pointer (&base));
     ret = PPD_PROBE_RESULT_SUCCESS;
   }
 
@@ -123,6 +150,21 @@ out:
   return ret;
 }
 
+static const char *
+profile_to_gov_pref (PpdProfile profile)
+{
+  switch (profile) {
+  case PPD_PROFILE_POWER_SAVER:
+    return "powersave";
+  case PPD_PROFILE_BALANCED:
+    return "powersave";
+  case PPD_PROFILE_PERFORMANCE:
+    return "performance";
+  }
+
+  g_assert_not_reached ();
+}
+
 static const char *
 profile_to_epp_pref (PpdProfile profile)
 {
@@ -142,16 +184,30 @@ profile_to_epp_pref (PpdProfile profile)
 
 static gboolean
 apply_pref_to_devices (GList       *devices,
-                       const char  *pref,
+                       PpdProfile   profile,
                        GError     **error)
 {
   gboolean ret = TRUE;
   GList *l;
 
   for (l = devices; l != NULL; l = l->next) {
-    const char *path = l->data;
+    const char *base = l->data;
+    g_autofree char *epp = NULL;
+    g_autofree char *gov = NULL;
 
-    ret = ppd_utils_write (path, pref, error);
+    gov = g_build_filename (base,
+                            "scaling_governor",
+                            NULL);
+
+    ret = ppd_utils_write (gov, profile_to_gov_pref (profile), error);
+    if (!ret)
+      break;
+
+    epp = g_build_filename (base,
+                            "energy_performance_preference",
+                            NULL);
+
+    ret = ppd_utils_write (epp, profile_to_epp_pref (profile), error);
     if (!ret)
       break;
   }
@@ -167,15 +223,20 @@ ppd_driver_amd_pstate_activate_profile (PpdDriver                    *driver,
 {
   PpdDriverAmdPstate *pstate = PPD_DRIVER_AMD_PSTATE (driver);
   gboolean ret = FALSE;
-  const char *pref;
 
   g_return_val_if_fail (pstate->epp_devices != NULL, FALSE);
 
   if (pstate->epp_devices) {
-    pref = profile_to_epp_pref (profile);
-    ret = apply_pref_to_devices (pstate->epp_devices, pref, error);
-    if (!ret)
+    ret = apply_pref_to_devices (pstate->epp_devices, profile, error);
+    if (!ret && pstate->activated_profile != PPD_PROFILE_UNSET) {
+      g_autoptr(GError) error_local = NULL;
+      /* reset back to previous */
+      if (!apply_pref_to_devices (pstate->epp_devices,
+                                  pstate->activated_profile,
+                                  &error_local))
+        g_warning ("failed to restore previous profile: %s", error_local->message);
       return ret;
+    }
   }
 
   if (ret)
@@ -200,11 +261,11 @@ ppd_driver_amd_pstate_class_init (PpdDriverAmdPstateClass *klass)
   GObjectClass *object_class;
   PpdDriverClass *driver_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->constructor = ppd_driver_amd_pstate_constructor;
   object_class->finalize = ppd_driver_amd_pstate_finalize;
 
-  driver_class = PPD_DRIVER_CLASS(klass);
+  driver_class = PPD_DRIVER_CLASS (klass);
   driver_class->probe = ppd_driver_amd_pstate_probe;
   driver_class->activate_profile = ppd_driver_amd_pstate_activate_profile;
 }
diff --git a/src/ppd-driver-amd-pstate.h b/src/ppd-driver-amd-pstate.h
index c4f1690..91f0577 100644
--- a/src/ppd-driver-amd-pstate.h
+++ b/src/ppd-driver-amd-pstate.h
@@ -10,7 +10,7 @@
 
 #pragma once
 
-#include "ppd-driver.h"
+#include "ppd-driver-cpu.h"
 
-#define PPD_TYPE_DRIVER_AMD_PSTATE (ppd_driver_amd_pstate_get_type())
-G_DECLARE_FINAL_TYPE(PpdDriverAmdPstate, ppd_driver_amd_pstate, PPD, DRIVER_AMD_PSTATE, PpdDriver)
+#define PPD_TYPE_DRIVER_AMD_PSTATE (ppd_driver_amd_pstate_get_type ())
+G_DECLARE_FINAL_TYPE (PpdDriverAmdPstate, ppd_driver_amd_pstate, PPD, DRIVER_AMD_PSTATE, PpdDriverCpu)
diff --git a/src/ppd-driver-cpu.c b/src/ppd-driver-cpu.c
new file mode 100644
index 0000000..d838193
--- /dev/null
+++ b/src/ppd-driver-cpu.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 Mario Limonciello <superm1 at gmail.com>
+ *
+ * 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
+ * the Free Software Foundation.
+ *
+ */
+
+#define G_LOG_DOMAIN "CpuDriver"
+
+#include "ppd-driver-cpu.h"
+
+/**
+ * SECTION:ppd-driver-cpu
+ * @Short_description: CPU Drivers
+ * @Title: CPU Profile Drivers
+ *
+ * Profile drivers are the implementation of the different profiles for
+ * the whole system. A driver will need to implement support for `power-saver`
+ * and `balanced` at a minimum.
+ *
+ * CPU drivers are typically used to change specifically the CPU efficiency
+ * to match the desired platform state.
+ */
+
+G_DEFINE_TYPE (PpdDriverCpu, ppd_driver_cpu, PPD_TYPE_DRIVER)
+
+static void
+ppd_driver_cpu_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (ppd_driver_cpu_parent_class)->finalize (object);
+}
+
+static void
+ppd_driver_cpu_class_init (PpdDriverCpuClass *klass)
+{
+  GObjectClass *object_class;
+
+  object_class = G_OBJECT_CLASS (klass);
+  object_class->finalize = ppd_driver_cpu_finalize;
+}
+
+static void
+ppd_driver_cpu_init (PpdDriverCpu *self)
+{
+}
diff --git a/src/ppd-driver-cpu.h b/src/ppd-driver-cpu.h
new file mode 100644
index 0000000..3ce3b2b
--- /dev/null
+++ b/src/ppd-driver-cpu.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 Mario Limonciello <superm1 at gmail.com>
+ *
+ * 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
+ * the Free Software Foundation.
+ *
+ */
+
+#pragma once
+
+#include "ppd-driver.h"
+
+#define PPD_TYPE_DRIVER_CPU (ppd_driver_cpu_get_type ())
+G_DECLARE_DERIVABLE_TYPE (PpdDriverCpu, ppd_driver_cpu, PPD, DRIVER_CPU, PpdDriver)
+
+/**
+ * PpdDriverCpuClass:
+ * @parent_class: The parent class.
+ *
+ * New CPU drivers should derive from #PpdDriverCpu and implement
+ * both @probe () and @activate_profile.
+ */
+struct _PpdDriverCpuClass
+{
+  PpdDriverClass   parent_class;
+};
diff --git a/src/ppd-driver-fake.c b/src/ppd-driver-fake.c
index ecd6154..6f53c26 100644
--- a/src/ppd-driver-fake.c
+++ b/src/ppd-driver-fake.c
@@ -18,7 +18,7 @@ void restart_profile_drivers (void);
 
 struct _PpdDriverFake
 {
-  PpdDriver  parent_instance;
+  PpdDriverPlatform  parent_instance;
 
   gboolean tio_set;
   struct termios old_tio;
@@ -27,7 +27,7 @@ struct _PpdDriverFake
   gboolean degraded;
 };
 
-G_DEFINE_TYPE (PpdDriverFake, ppd_driver_fake, PPD_TYPE_DRIVER)
+G_DEFINE_TYPE (PpdDriverFake, ppd_driver_fake, PPD_TYPE_DRIVER_PLATFORM)
 
 static GObject*
 ppd_driver_fake_constructor (GType                  type,
@@ -107,10 +107,10 @@ setup_keyboard (PpdDriverFake *fake)
 {
   struct termios new_tio;
 
-  tcgetattr(STDIN_FILENO, &fake->old_tio);
+  tcgetattr (STDIN_FILENO, &fake->old_tio);
   new_tio = fake->old_tio;
   new_tio.c_lflag &=(~ICANON & ~ECHO);
-  tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
+  tcsetattr (STDIN_FILENO, TCSANOW, &new_tio);
 
   fake->channel = g_io_channel_unix_new (STDIN_FILENO);
   if (!fake->channel) {
@@ -150,6 +150,10 @@ ppd_driver_fake_probe (PpdDriver  *driver)
   if (!envvar_set ("POWER_PROFILE_DAEMON_FAKE_DRIVER"))
     return PPD_PROBE_RESULT_FAIL;
 
+  /* don't activate stdin unless interactive */
+  if (isatty (fileno (stdout)) == 0)
+    return PPD_PROBE_RESULT_SUCCESS;
+
   fake = PPD_DRIVER_FAKE (driver);
   if (!setup_keyboard (fake))
     return PPD_PROBE_RESULT_FAIL;
@@ -180,7 +184,7 @@ ppd_driver_fake_finalize (GObject *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);
+    tcsetattr (STDIN_FILENO, TCSANOW, &fake->old_tio);
   G_OBJECT_CLASS (ppd_driver_fake_parent_class)->finalize (object);
 }
 
@@ -190,11 +194,11 @@ ppd_driver_fake_class_init (PpdDriverFakeClass *klass)
   GObjectClass *object_class;
   PpdDriverClass *driver_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->constructor = ppd_driver_fake_constructor;
   object_class->finalize = ppd_driver_fake_finalize;
 
-  driver_class = PPD_DRIVER_CLASS(klass);
+  driver_class = PPD_DRIVER_CLASS (klass);
   driver_class->probe = ppd_driver_fake_probe;
   driver_class->activate_profile = ppd_driver_fake_activate_profile;
 }
diff --git a/src/ppd-driver-fake.h b/src/ppd-driver-fake.h
index 6ab2f82..4e3e2e7 100644
--- a/src/ppd-driver-fake.h
+++ b/src/ppd-driver-fake.h
@@ -9,7 +9,7 @@
 
 #pragma once
 
-#include "ppd-driver.h"
+#include "ppd-driver-platform.h"
 
-#define PPD_TYPE_DRIVER_FAKE (ppd_driver_fake_get_type())
-G_DECLARE_FINAL_TYPE(PpdDriverFake, ppd_driver_fake, PPD, DRIVER_FAKE, PpdDriver)
+#define PPD_TYPE_DRIVER_FAKE (ppd_driver_fake_get_type ())
+G_DECLARE_FINAL_TYPE (PpdDriverFake, ppd_driver_fake, PPD, DRIVER_FAKE, PpdDriverPlatform)
diff --git a/src/ppd-driver-intel-pstate.c b/src/ppd-driver-intel-pstate.c
index 413a0d2..d859b98 100644
--- a/src/ppd-driver-intel-pstate.c
+++ b/src/ppd-driver-intel-pstate.c
@@ -7,6 +7,8 @@
  *
  */
 
+#define G_LOG_DOMAIN "CpuDriver"
+
 #include <upower.h>
 
 #include "ppd-utils.h"
@@ -25,7 +27,7 @@
 
 struct _PpdDriverIntelPstate
 {
-  PpdDriver  parent_instance;
+  PpdDriverCpu  parent_instance;
 
   PpdProfile activated_profile;
   GList *epp_devices; /* GList of paths */
@@ -35,7 +37,7 @@ struct _PpdDriverIntelPstate
   char *no_turbo_path;
 };
 
-G_DEFINE_TYPE (PpdDriverIntelPstate, ppd_driver_intel_pstate, PPD_TYPE_DRIVER)
+G_DEFINE_TYPE (PpdDriverIntelPstate, ppd_driver_intel_pstate, PPD_TYPE_DRIVER_CPU)
 
 static gboolean ppd_driver_intel_pstate_activate_profile (PpdDriver                   *driver,
                                                           PpdProfile                   profile,
@@ -108,7 +110,7 @@ monitor_no_turbo_prop (const char *path)
 }
 
 static gboolean
-has_turbo (void)
+sys_has_turbo (void)
 {
   g_autofree char *turbo_pct_path = NULL;
   g_autofree char *contents = NULL;
@@ -265,17 +267,18 @@ ppd_driver_intel_pstate_probe (PpdDriver  *driver)
 {
   PpdDriverIntelPstate *pstate = PPD_DRIVER_INTEL_PSTATE (driver);
   PpdProbeResult ret = PPD_PROBE_RESULT_FAIL;
+  PpdProbeResult epp_ret, epb_ret;
+  gboolean has_turbo;
 
-  ret = probe_epp (pstate);
-  if (ret == PPD_PROBE_RESULT_SUCCESS)
-    probe_epb (pstate);
-  else
-    ret = probe_epb (pstate);
+  epp_ret = probe_epp (pstate);
+  epb_ret = probe_epb (pstate);
+  ret = (epp_ret == PPD_PROBE_RESULT_SUCCESS) ? epp_ret : epb_ret;
 
   if (ret != PPD_PROBE_RESULT_SUCCESS)
     goto out;
 
-  if (has_turbo ()) {
+  has_turbo = sys_has_turbo ();
+  if (has_turbo) {
     /* Monitor the first "no_turbo" */
     pstate->no_turbo_path = ppd_utils_get_sysfs_path (NO_TURBO_PATH);
     pstate->no_turbo_mon = monitor_no_turbo_prop (pstate->no_turbo_path);
@@ -287,8 +290,15 @@ ppd_driver_intel_pstate_probe (PpdDriver  *driver)
   }
 
 out:
-  g_debug ("%s p-state settings",
+  g_debug ("%s Intel p-state settings",
            ret == PPD_PROBE_RESULT_SUCCESS ? "Found" : "Didn't find");
+  if (ret == PPD_PROBE_RESULT_SUCCESS) {
+    g_debug ("\tEnergy Performance Preference: %s",
+             epp_ret == PPD_PROBE_RESULT_SUCCESS ? "yes" : "no");
+    g_debug ("\tEnergy Performance Bias: %s",
+             epp_ret == PPD_PROBE_RESULT_SUCCESS ? "yes" : "no");
+    g_debug ("\tHas Turbo: %s", has_turbo ? "yes" : "no");
+  }
   return ret;
 }
 
@@ -395,11 +405,11 @@ ppd_driver_intel_pstate_class_init (PpdDriverIntelPstateClass *klass)
   GObjectClass *object_class;
   PpdDriverClass *driver_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->constructor = ppd_driver_intel_pstate_constructor;
   object_class->finalize = ppd_driver_intel_pstate_finalize;
 
-  driver_class = PPD_DRIVER_CLASS(klass);
+  driver_class = PPD_DRIVER_CLASS (klass);
   driver_class->probe = ppd_driver_intel_pstate_probe;
   driver_class->activate_profile = ppd_driver_intel_pstate_activate_profile;
 }
diff --git a/src/ppd-driver-intel-pstate.h b/src/ppd-driver-intel-pstate.h
index 42f8cf2..e1c7d20 100644
--- a/src/ppd-driver-intel-pstate.h
+++ b/src/ppd-driver-intel-pstate.h
@@ -9,7 +9,7 @@
 
 #pragma once
 
-#include "ppd-driver.h"
+#include "ppd-driver-cpu.h"
 
-#define PPD_TYPE_DRIVER_INTEL_PSTATE (ppd_driver_intel_pstate_get_type())
-G_DECLARE_FINAL_TYPE(PpdDriverIntelPstate, ppd_driver_intel_pstate, PPD, DRIVER_INTEL_PSTATE, PpdDriver)
+#define PPD_TYPE_DRIVER_INTEL_PSTATE (ppd_driver_intel_pstate_get_type ())
+G_DECLARE_FINAL_TYPE (PpdDriverIntelPstate, ppd_driver_intel_pstate, PPD, DRIVER_INTEL_PSTATE, PpdDriverCpu)
diff --git a/src/ppd-driver-placeholder.c b/src/ppd-driver-placeholder.c
index 11d9e93..e0f8021 100644
--- a/src/ppd-driver-placeholder.c
+++ b/src/ppd-driver-placeholder.c
@@ -7,14 +7,16 @@
  *
  */
 
+#define G_LOG_DOMAIN "PlatformDriver"
+
 #include "ppd-driver-placeholder.h"
 
 struct _PpdDriverPlaceholder
 {
-  PpdDriver  parent_instance;
+  PpdDriverPlatform  parent_instance;
 };
 
-G_DEFINE_TYPE (PpdDriverPlaceholder, ppd_driver_placeholder, PPD_TYPE_DRIVER)
+G_DEFINE_TYPE (PpdDriverPlaceholder, ppd_driver_placeholder, PPD_TYPE_DRIVER_PLATFORM)
 
 static GObject*
 ppd_driver_placeholder_constructor (GType                  type,
@@ -39,7 +41,7 @@ ppd_driver_placeholder_class_init (PpdDriverPlaceholderClass *klass)
 {
   GObjectClass *object_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->constructor = ppd_driver_placeholder_constructor;
 }
 
diff --git a/src/ppd-driver-placeholder.h b/src/ppd-driver-placeholder.h
index a822790..5fb49fb 100644
--- a/src/ppd-driver-placeholder.h
+++ b/src/ppd-driver-placeholder.h
@@ -9,7 +9,7 @@
 
 #pragma once
 
-#include "ppd-driver.h"
+#include "ppd-driver-platform.h"
 
-#define PPD_TYPE_DRIVER_PLACEHOLDER (ppd_driver_placeholder_get_type())
-G_DECLARE_FINAL_TYPE(PpdDriverPlaceholder, ppd_driver_placeholder, PPD, DRIVER_PLACEHOLDER, PpdDriver)
+#define PPD_TYPE_DRIVER_PLACEHOLDER (ppd_driver_placeholder_get_type ())
+G_DECLARE_FINAL_TYPE (PpdDriverPlaceholder, ppd_driver_placeholder, PPD, DRIVER_PLACEHOLDER, PpdDriverPlatform)
diff --git a/src/ppd-driver-platform-profile.c b/src/ppd-driver-platform-profile.c
index aebc655..6b53c4d 100644
--- a/src/ppd-driver-platform-profile.c
+++ b/src/ppd-driver-platform-profile.c
@@ -7,6 +7,8 @@
  *
  */
 
+#define G_LOG_DOMAIN "PlatformDriver"
+
 #include <gudev/gudev.h>
 #include <gio/gio.h>
 
@@ -32,7 +34,7 @@ struct _PpdDriverPlatformProfile
   guint acpi_platform_profile_changed_id;
 };
 
-G_DEFINE_TYPE (PpdDriverPlatformProfile, ppd_driver_platform_profile, PPD_TYPE_DRIVER)
+G_DEFINE_TYPE (PpdDriverPlatformProfile, ppd_driver_platform_profile, PPD_TYPE_DRIVER_PLATFORM)
 
 static GObject*
 ppd_driver_platform_profile_constructor (GType                  type,
@@ -345,11 +347,11 @@ ppd_driver_platform_profile_class_init (PpdDriverPlatformProfileClass *klass)
   GObjectClass *object_class;
   PpdDriverClass *driver_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->constructor = ppd_driver_platform_profile_constructor;
   object_class->finalize = ppd_driver_platform_profile_finalize;
 
-  driver_class = PPD_DRIVER_CLASS(klass);
+  driver_class = PPD_DRIVER_CLASS (klass);
   driver_class->probe = ppd_driver_platform_profile_probe;
   driver_class->activate_profile = ppd_driver_platform_profile_activate_profile;
 }
diff --git a/src/ppd-driver-platform-profile.h b/src/ppd-driver-platform-profile.h
index 916fd7e..5ac7eda 100644
--- a/src/ppd-driver-platform-profile.h
+++ b/src/ppd-driver-platform-profile.h
@@ -9,7 +9,7 @@
 
 #pragma once
 
-#include "ppd-driver.h"
+#include "ppd-driver-platform.h"
 
-#define PPD_TYPE_DRIVER_PLATFORM_PROFILE (ppd_driver_platform_profile_get_type())
-G_DECLARE_FINAL_TYPE(PpdDriverPlatformProfile, ppd_driver_platform_profile, PPD, DRIVER_PLATFORM_PROFILE, PpdDriver)
+#define PPD_TYPE_DRIVER_PLATFORM_PROFILE (ppd_driver_platform_profile_get_type ())
+G_DECLARE_FINAL_TYPE (PpdDriverPlatformProfile, ppd_driver_platform_profile, PPD, DRIVER_PLATFORM_PROFILE, PpdDriverPlatform)
diff --git a/src/ppd-driver-platform.c b/src/ppd-driver-platform.c
new file mode 100644
index 0000000..33dc955
--- /dev/null
+++ b/src/ppd-driver-platform.c
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 Mario Limonciello <superm1 at gmail.com>
+ *
+ * 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
+ * the Free Software Foundation.
+ *
+ */
+
+#define G_LOG_DOMAIN "PlatformDriver"
+
+#include "ppd-driver-platform.h"
+
+G_DEFINE_TYPE (PpdDriverPlatform, ppd_driver_platform, PPD_TYPE_DRIVER)
+
+/**
+ * SECTION:ppd-driver-platform
+ * @Short_description: Profile Drivers
+ * @Title: Platform Profile Drivers
+ *
+ * Platform drivers are the implementation of the different profiles for
+ * the whole system. A driver will need to implement support for `power-saver`
+ * and `balanced` at a minimum.
+ *
+ * If no system-specific platform driver is available, a placeholder driver
+ * will be put in place, and the `performance` profile will be unavailable.
+ *
+ * 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-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.
+ */
+
+static void
+ppd_driver_platform_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (ppd_driver_platform_parent_class)->finalize (object);
+}
+
+static void
+ppd_driver_platform_class_init (PpdDriverPlatformClass *klass)
+{
+  GObjectClass *object_class;
+
+  object_class = G_OBJECT_CLASS (klass);
+  object_class->finalize = ppd_driver_platform_finalize;
+}
+
+static void
+ppd_driver_platform_init (PpdDriverPlatform *self)
+{
+}
diff --git a/src/ppd-driver-platform.h b/src/ppd-driver-platform.h
new file mode 100644
index 0000000..9dbd247
--- /dev/null
+++ b/src/ppd-driver-platform.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 Mario Limonciello <superm1 at gmail.com>
+ *
+ * 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
+ * the Free Software Foundation.
+ *
+ */
+
+#pragma once
+
+#include "ppd-driver.h"
+
+#define PPD_TYPE_DRIVER_PLATFORM (ppd_driver_platform_get_type ())
+G_DECLARE_DERIVABLE_TYPE (PpdDriverPlatform, ppd_driver_platform, PPD, DRIVER_PLATFORM, PpdDriver)
+
+/**
+ * PpdDriverPlatformClass:
+ * @parent_class: The parent class.
+ *
+ * New Platform drivers should derive from #PpdDriverPlatform and implement
+ * at least one of @probe () and @activate_profile.
+ */
+struct _PpdDriverPlatformClass
+{
+  PpdDriverClass   parent_class;
+};
diff --git a/src/ppd-driver.c b/src/ppd-driver.c
index 9a1f369..435e9e9 100644
--- a/src/ppd-driver.c
+++ b/src/ppd-driver.c
@@ -19,13 +19,7 @@
  * the whole system. A driver will need to implement support `power-saver`
  * and `balanced` at a minimum.
  *
- * If no system-specific driver is available, a placeholder driver
- * will be put in place, and the `performance` profile will be unavailable.
- *
- * 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.
+ * All drivers should be derived from either #PpdDriverCpu or #PpdDriverPlatform
  *
  * When a driver implements the `performance` profile, it might set the
  * #PpdDriver:performance-degraded property if the profile isn't running to
@@ -78,11 +72,17 @@ ppd_driver_set_property (GObject        *object,
     priv->profiles = g_value_get_flags (value);
     break;
   case PROP_PERFORMANCE_DEGRADED:
-    g_clear_pointer (&priv->performance_degraded, g_free);
-    priv->performance_degraded = g_value_dup_string (value);
+    {
+      const char *degraded = g_value_get_string (value);
+
+      g_clear_pointer (&priv->performance_degraded, g_free);
+
+      if (degraded != NULL && *degraded != '\0')
+        priv->performance_degraded = g_strdup (degraded);
+    }
     break;
   default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
   }
 }
 
@@ -106,7 +106,7 @@ ppd_driver_get_property (GObject        *object,
     g_value_set_string (value, priv->performance_degraded);
     break;
   default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
   }
 }
 
@@ -127,7 +127,7 @@ ppd_driver_class_init (PpdDriverClass *klass)
 {
   GObjectClass *object_class;
 
-  object_class = G_OBJECT_CLASS(klass);
+  object_class = G_OBJECT_CLASS (klass);
   object_class->finalize = ppd_driver_finalize;
   object_class->get_property = ppd_driver_get_property;
   object_class->set_property = ppd_driver_set_property;
@@ -173,7 +173,7 @@ ppd_driver_class_init (PpdDriverClass *klass)
    * A unique driver name, only used for debugging.
    */
   g_object_class_install_property (object_class, PROP_DRIVER_NAME,
-                                   g_param_spec_string("driver-name",
+                                   g_param_spec_string ("driver-name",
                                                        "Driver name",
                                                        "Profile driver name",
                                                        NULL,
@@ -185,21 +185,20 @@ ppd_driver_class_init (PpdDriverClass *klass)
    * The bitmask of #PpdProfile<!-- -->s implemented by this driver.
    */
   g_object_class_install_property (object_class, PROP_PROFILES,
-                                   g_param_spec_flags("profiles",
+                                   g_param_spec_flags ("profiles",
                                                       "Profiles",
                                                       "Profiles implemented by this driver",
                                                       PPD_TYPE_PROFILE,
                                                       0,
                                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
-
   /**
-   * PpdDriver:performance-degraded:
+   * PpdPlatformDriver: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_DEGRADED,
-                                   g_param_spec_string("performance-degraded",
+                                   g_param_spec_string ("performance-degraded",
                                                        "Performance Degraded",
                                                        "Why the performance profile is degraded, if set",
                                                        NULL,
@@ -278,7 +277,7 @@ ppd_driver_get_performance_degraded (PpdDriver *driver)
   g_return_val_if_fail (PPD_IS_DRIVER (driver), NULL);
 
   priv = PPD_DRIVER_GET_PRIVATE (driver);
-  return priv->performance_degraded ? priv->performance_degraded : "";
+  return priv->performance_degraded;
 }
 
 gboolean
diff --git a/src/ppd-driver.h b/src/ppd-driver.h
index f1e0f63..f02bd4d 100644
--- a/src/ppd-driver.h
+++ b/src/ppd-driver.h
@@ -12,26 +12,8 @@
 #include <glib-object.h>
 #include "ppd-profile.h"
 
-#define PPD_TYPE_DRIVER (ppd_driver_get_type())
-G_DECLARE_DERIVABLE_TYPE(PpdDriver, ppd_driver, PPD, DRIVER, GObject)
-
-/**
- * PpdProbeResult:
- * @PPD_PROBE_RESULT_UNSET: unset
- * @PPD_PROBE_RESULT_DEFER: driver should be kept alive, as kernel
- *   support might appear.
- * @PPD_PROBE_RESULT_FAIL: driver failed to load.
- * @PPD_PROBE_RESULT_SUCCESS: driver successfully loaded.
- *
- * Those are the three possible values returned by a driver probe,
- * along with an unset value for convenience.
- */
-typedef enum {
-  PPD_PROBE_RESULT_UNSET = -2,
-  PPD_PROBE_RESULT_DEFER = -1,
-  PPD_PROBE_RESULT_FAIL = 0,
-  PPD_PROBE_RESULT_SUCCESS = 1
-} PpdProbeResult;
+#define PPD_TYPE_DRIVER (ppd_driver_get_type ())
+G_DECLARE_DERIVABLE_TYPE (PpdDriver, ppd_driver, PPD, DRIVER, GObject)
 
 /**
  * PpdProfileActivationReason:
@@ -64,8 +46,9 @@ typedef enum{
  * @probe: Called by the daemon on startup.
  * @activate_profile: Called by the daemon for every profile change.
  *
- * New profile drivers should derive from #PpdDriver and implement
- * at least one of probe() and @activate_profile.
+ * New profile drivers should not derive from #PpdDriver.  They should
+ * derive from the child from #PpdDriverCpu or #PpdDriverPlatform drivers
+ * and implement at least one of probe () and @activate_profile.
  */
 struct _PpdDriverClass
 {
diff --git a/src/ppd-profile.h b/src/ppd-profile.h
index 03c04eb..4af46ad 100644
--- a/src/ppd-profile.h
+++ b/src/ppd-profile.h
@@ -11,6 +11,24 @@
 
 #include <glib.h>
 
+/**
+ * PpdProbeResult:
+ * @PPD_PROBE_RESULT_UNSET: unset
+ * @PPD_PROBE_RESULT_DEFER: driver should be kept alive, as kernel
+ *   support might appear.
+ * @PPD_PROBE_RESULT_FAIL: driver failed to load.
+ * @PPD_PROBE_RESULT_SUCCESS: driver successfully loaded.
+ *
+ * Those are the three possible values returned by a driver probe,
+ * along with an unset value for convenience.
+ */
+typedef enum {
+  PPD_PROBE_RESULT_UNSET = -2,
+  PPD_PROBE_RESULT_DEFER = -1,
+  PPD_PROBE_RESULT_FAIL = 0,
+  PPD_PROBE_RESULT_SUCCESS = 1
+} PpdProbeResult;
+
 /**
  * PpdProfile:
  * @PPD_PROFILE_POWER_SAVER: "power-saver", the battery saving profile
diff --git a/src/ppd-utils.c b/src/ppd-utils.c
index 5ac732f..6f6441b 100644
--- a/src/ppd-utils.c
+++ b/src/ppd-utils.c
@@ -7,6 +7,8 @@
  *
  */
 
+#define G_LOG_DOMAIN "Utils"
+
 #include "ppd-utils.h"
 #include <gio/gio.h>
 #include <stdio.h>
@@ -43,7 +45,7 @@ gboolean ppd_utils_write (const char  *filename,
     g_debug ("Could not open for writing '%s'", filename);
     return FALSE;
   }
-  setbuf(sysfsfp, NULL);
+  setbuf (sysfsfp, NULL);
   ret = fprintf (sysfsfp, "%s", value);
   if (ret <= 0) {
     g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno),
@@ -75,6 +77,17 @@ gboolean ppd_utils_write_sysfs (GUdevDevice  *device,
   return ppd_utils_write (filename, value, error);
 }
 
+gboolean ppd_utils_write_sysfs_int (GUdevDevice  *device,
+                                    const char   *attribute,
+                                    gint64        value,
+                                    GError      **error)
+{
+  g_autofree char *str_value = NULL;
+
+  str_value = g_strdup_printf ("%" G_GINT64_FORMAT, value);
+  return ppd_utils_write_sysfs (device, attribute, str_value, error);
+}
+
 GFileMonitor *
 ppd_utils_monitor_sysfs_attr (GUdevDevice  *device,
                               const char   *attribute,
diff --git a/src/ppd-utils.h b/src/ppd-utils.h
index 9b12e5b..5ba8b8d 100644
--- a/src/ppd-utils.h
+++ b/src/ppd-utils.h
@@ -20,6 +20,10 @@ gboolean ppd_utils_write_sysfs (GUdevDevice  *device,
                                 const char   *attribute,
                                 const char   *value,
                                 GError      **error);
+gboolean ppd_utils_write_sysfs_int (GUdevDevice  *device,
+                                    const char   *attribute,
+                                    gint64        value,
+                                    GError      **error);
 GFileMonitor *ppd_utils_monitor_sysfs_attr (GUdevDevice  *device,
                                             const char   *attribute,
                                             GError      **error);
diff --git a/tests/integration-test.py b/tests/integration-test.py
deleted file mode 100755
index 8e37f69..0000000
--- a/tests/integration-test.py
+++ /dev/null
@@ -1,1264 +0,0 @@
-#!/usr/bin/python3
-
-# power-profiles-daemon integration test suite
-#
-# Run in built tree to test local built binaries, or from anywhere else to test
-# system installed binaries.
-#
-# 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
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-
-import os
-import sys
-import dbus
-import tempfile
-import subprocess
-import unittest
-import time
-
-try:
-    import gi
-    from gi.repository import GLib
-    from gi.repository import Gio
-except ImportError as e:
-    sys.stderr.write('Skipping tests, PyGobject not available for Python 3, or missing GI typelibs: %s\n' % str(e))
-    sys.exit(77)
-
-try:
-    gi.require_version('UMockdev', '1.0')
-    from gi.repository import UMockdev
-except ImportError:
-    sys.stderr.write('Skipping tests, umockdev not available (https://github.com/martinpitt/umockdev)\n')
-    sys.exit(77)
-
-try:
-    import dbusmock
-except ImportError:
-    sys.stderr.write('Skipping tests, python-dbusmock not available (http://pypi.python.org/pypi/python-dbusmock).\n')
-    sys.exit(77)
-
-
-PP = 'net.hadess.PowerProfiles'
-PP_PATH = '/net/hadess/PowerProfiles'
-PP_INTERFACE = 'net.hadess.PowerProfiles'
-
-class Tests(dbusmock.DBusTestCase):
-    @classmethod
-    def setUpClass(cls):
-        # run from local build tree if we are in one, otherwise use system instance
-        builddir = os.getenv('top_builddir', '.')
-        if os.access(os.path.join(builddir, 'src', 'power-profiles-daemon'), os.X_OK):
-            cls.daemon_path = os.path.join(builddir, 'src', 'power-profiles-daemon')
-            print('Testing binaries from local build tree (%s)' % cls.daemon_path)
-        elif os.environ.get('UNDER_JHBUILD', False):
-            jhbuild_prefix = os.environ['JHBUILD_PREFIX']
-            cls.daemon_path = os.path.join(jhbuild_prefix, 'libexec', 'power-profiles-daemon')
-            print('Testing binaries from JHBuild (%s)' % cls.daemon_path)
-        else:
-            cls.daemon_path = None
-            with open('/usr/lib/systemd/system/power-profiles-daemon.service') as f:
-                for line in f:
-                    if line.startswith('ExecStart='):
-                        cls.daemon_path = line.split('=', 1)[1].strip()
-                        break
-            assert cls.daemon_path, 'could not determine daemon path from systemd .service file'
-            print('Testing installed system binary (%s)' % cls.daemon_path)
-
-        # fail on CRITICALs on client and server side
-        GLib.log_set_always_fatal(GLib.LogLevelFlags.LEVEL_WARNING |
-                                  GLib.LogLevelFlags.LEVEL_ERROR |
-                                  GLib.LogLevelFlags.LEVEL_CRITICAL)
-        os.environ['G_DEBUG'] = 'fatal_warnings'
-
-        # set up a fake system D-BUS
-        cls.test_bus = Gio.TestDBus.new(Gio.TestDBusFlags.NONE)
-        cls.test_bus.up()
-        try:
-            del os.environ['DBUS_SESSION_BUS_ADDRESS']
-        except KeyError:
-            pass
-        os.environ['DBUS_SYSTEM_BUS_ADDRESS'] = cls.test_bus.get_bus_address()
-
-        cls.dbus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
-        cls.dbus_con = cls.get_dbus(True)
-
-    @classmethod
-    def tearDownClass(cls):
-        cls.test_bus.down()
-        dbusmock.DBusTestCase.tearDownClass()
-
-    def setUp(self):
-        '''Set up a local umockdev testbed.
-
-        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
-        self.daemon = None
-
-        # Used for dytc devices
-        self.tp_acpi = None
-
-    def run(self, result=None):
-        super(Tests, self).run(result)
-        if result and len(result.errors) + len(result.failures) > 0 and self.log:
-            with open(self.log.name) as f:
-                sys.stderr.write('\n-------------- daemon log: ----------------\n')
-                sys.stderr.write(f.read())
-                sys.stderr.write('------------------------------\n')
-
-    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
-
-    #
-    # Daemon control and D-BUS I/O
-    #
-
-    def start_daemon(self):
-        '''Start daemon and create DBus proxy.
-
-        When done, this sets self.proxy as the Gio.DBusProxy for power-profiles-daemon.
-        '''
-        env = os.environ.copy()
-        env['G_DEBUG'] = 'fatal-criticals'
-        env['G_MESSAGES_DEBUG'] = 'all'
-        # note: Python doesn't propagate the setenv from Testbed.new(), so we
-        # have to do that ourselves
-        env['UMOCKDEV_DIR'] = self.testbed.get_root_dir()
-        self.log = tempfile.NamedTemporaryFile()
-        if os.getenv('VALGRIND') != None:
-            daemon_path = ['valgrind', self.daemon_path, '-v']
-        else:
-            daemon_path = [self.daemon_path, '-v']
-
-        self.daemon = subprocess.Popen(daemon_path,
-                                       env=env, stdout=self.log,
-                                       stderr=subprocess.STDOUT)
-
-        # wait until the daemon gets online
-        timeout = 100
-        while timeout > 0:
-            time.sleep(0.1)
-            timeout -= 1
-            try:
-                self.get_dbus_property('ActiveProfile')
-                break
-            except GLib.GError:
-                pass
-        else:
-            self.fail('daemon did not start in 10 seconds')
-
-        self.proxy = Gio.DBusProxy.new_sync(
-            self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None, PP,
-            PP_PATH, PP, None)
-
-        self.assertEqual(self.daemon.poll(), None, 'daemon crashed')
-
-    def stop_daemon(self):
-        '''Stop the daemon if it is running.'''
-
-        if self.daemon:
-            try:
-                self.daemon.kill()
-            except OSError:
-                pass
-            self.daemon.wait()
-        self.daemon = None
-        self.proxy = None
-
-    def get_dbus_property(self, name):
-        '''Get property value from daemon D-Bus interface.'''
-
-        proxy = Gio.DBusProxy.new_sync(
-            self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None, PP,
-            PP_PATH, 'org.freedesktop.DBus.Properties', None)
-        return proxy.Get('(ss)', PP, name)
-
-    def set_dbus_property(self, name, value):
-        '''Set property value on daemon D-Bus interface.'''
-
-        proxy = Gio.DBusProxy.new_sync(
-            self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None, PP,
-            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
-
-    def count_text_in_log(self, text):
-        with open(self.log.name) as f:
-            return f.read().count(text)
-
-    def read_sysfs_file(self, path):
-        with open(self.testbed.get_root_dir() + '/' + path, 'rb') as f:
-          return f.read().rstrip()
-        return None
-
-    def read_sysfs_attr(self, device, attribute):
-        return self.read_sysfs_file(device + '/' + attribute)
-
-    def get_mtime(self, device, attribute):
-        return os.path.getmtime(self.testbed.get_root_dir() + '/' + device + '/' + attribute)
-
-    def read_file(self, path):
-        with open(path, 'rb') as f:
-            return f.read()
-        return None
-
-    def create_dytc_device(self):
-      self.tp_acpi = self.testbed.add_device('platform', 'thinkpad_acpi', None,
-          ['dytc_lapmode', '0\n'],
-          [ '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:
-        profile.write("performance\n")
-      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.
-
-        Timeout is in deciseconds, defaulting to 50 (5 seconds). message is
-        printed on failure.
-        '''
-        while timeout >= 0:
-            context = GLib.MainContext.default()
-            while context.iteration(False):
-                pass
-            if condition():
-                break
-            timeout -= 1
-            time.sleep(0.1)
-        else:
-            self.fail(message or 'timed out waiting for ' + str(condition))
-
-    #
-    # Actual test cases
-    #
-    def test_dbus_startup_error(self):
-      '''D-Bus startup error'''
-
-      self.start_daemon()
-      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()
-
-    def test_no_performance_driver(self):
-      '''no performance driver'''
-
-      self.start_daemon()
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), '')
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 2)
-      self.assertEqual(profiles[1]['Driver'], 'placeholder')
-      self.assertEqual(profiles[0]['Driver'], 'placeholder')
-      self.assertEqual(profiles[1]['Profile'], 'balanced')
-      self.assertEqual(profiles[0]['Profile'], 'power-saver')
-
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-      with self.assertRaises(gi.repository.GLib.GError):
-        self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-      with self.assertRaises(gi.repository.GLib.GError):
-        cookie = self.call_dbus_method('HoldProfile', GLib.Variant("(sss)", ('performance', 'testReason', 'testApplication')))
-
-      # process = subprocess.Popen(['gdbus', 'introspect', '--system', '--dest', 'net.hadess.PowerProfiles', '--object-path', '/net/hadess/PowerProfiles'])
-      # print (self.get_dbus_property('GPUs'))
-
-      self.stop_daemon()
-
-    def test_inhibited_property(self):
-      '''Test that the inhibited property exists'''
-
-      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('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('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-
-      # 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('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'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-    def test_intel_pstate(self):
-      '''Intel P-State driver (no UPower)'''
-
-      # 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, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      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, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      with open(os.path.join(dir2, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-
-      # Create Intel P-State configuration
-      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:
-        no_turbo.write("0\n")
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("active\n")
-
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'intel_pstate')
-      self.assertEqual(profiles[0]['Profile'], 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir2, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'balance_performance')
-
-      # Set performance mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-
-      contents = None
-      with open(os.path.join(dir2, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'performance')
-
-      # Disable 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'), 'performance')
-      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), 'high-operating-temperature')
-
-      self.stop_daemon()
-
-      # Verify that the Lenovo DYTC driver still gets preferred
-      self.create_platform_profile()
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'platform_profile')
-
-    def test_intel_pstate_balance(self):
-      '''Intel P-State driver (balance)'''
-
-      # Create CPU with preference
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      gov_path = os.path.join(dir1, 'scaling_governor')
-      with open(gov_path, 'w') as gov:
-        gov.write('performance\n')
-      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-      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, "status"),'w') as status:
-        status.write("active\n")
-
-      upowerd, obj_upower = self.spawn_server_template(
-            'upower', {'DaemonVersion': '0.99', 'OnBattery': False}, stdout=subprocess.PIPE)
-
-      self.start_daemon()
-
-      with open(gov_path, 'rb') as f:
-        contents = f.read()
-        self.assertEqual(contents, b'powersave')
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'intel_pstate')
-      self.assertEqual(profiles[0]['Profile'], 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      # This matches what's written by ppd-driver-intel-pstate.c
-      self.assertEqual(contents, b'balance_performance')
-
-      self.stop_daemon()
-
-      upowerd.terminate()
-      upowerd.wait()
-      upowerd.stdout.close()
-
-    def test_intel_pstate_error(self):
-      '''Intel P-State driver in error state'''
-
-      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, "status"),'w') as status:
-        status.write("active\n")
-
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      with open(os.path.join(dir1, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      pref_path = os.path.join(dir1, "energy_performance_preference")
-      old_umask = os.umask(0o333)
-      with open(pref_path,'w') as prefs:
-        prefs.write("balance_performance\n")
-      os.umask(old_umask)
-      # Make file non-writable to root
-      if os.geteuid() == 0:
-        if not GLib.find_program_in_path('chattr'):
-          os._exit(77)
-        subprocess.check_output(['chattr', '+i', pref_path])
-
-      self.start_daemon()
-
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      # Error when setting performance mode
-      with self.assertRaises(gi.repository.GLib.GError):
-        self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'balance_performance\n')
-
-      self.stop_daemon()
-
-      if os.geteuid() == 0:
-        subprocess.check_output(['chattr', '-i', pref_path])
-
-    def test_intel_pstate_passive(self):
-      '''Intel P-State in passive mode -> placeholder'''
-
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      with open(os.path.join(dir1, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-
-      # Create Intel P-State configuration
-      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:
-        no_turbo.write("0\n")
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("passive\n")
-
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 2)
-      self.assertEqual(profiles[0]['Driver'], 'placeholder')
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'performance\n')
-
-      # Set performance mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'performance\n')
-
-      self.stop_daemon()
-
-    def test_intel_pstate_passive_with_epb(self):
-      '''Intel P-State in passive mode (no HWP) with energy_perf_bias'''
-
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      with open(os.path.join(dir1, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      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/cpu0/power/")
-      os.makedirs(dir2)
-      with open(os.path.join(dir2, 'energy_perf_bias'), 'w') as epb:
-        epb.write("6")
-
-      # Create Intel P-State configuration
-      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:
-        no_turbo.write("0\n")
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("passive\n")
-
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'intel_pstate')
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      # Set power-saver mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir2, "energy_perf_bias"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'15')
-
-      # Set performance mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-
-      contents = None
-      with open(os.path.join(dir2, "energy_perf_bias"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'0')
-
-      self.stop_daemon()
-
-    def test_amd_pstate(self):
-      '''AMD P-State driver (no UPower)'''
-
-      # 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, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      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, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      with open(os.path.join(dir2, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-
-      # Create AMD P-State configuration
-      pstate_dir = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate")
-      os.makedirs(pstate_dir)
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("active\n")
-
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'amd_pstate')
-      self.assertEqual(profiles[0]['Profile'], 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir2, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'balance_performance')
-
-      # Set performance mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-
-      contents = None
-      with open(os.path.join(dir2, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'performance')
-
-      self.stop_daemon()
-
-      # Verify that the Lenovo DYTC driver still gets preferred
-      self.create_platform_profile()
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'platform_profile')
-
-    def test_amd_pstate_balance(self):
-      '''AMD P-State driver (balance)'''
-
-      # Create CPU with preference
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      gov_path = os.path.join(dir1, 'scaling_governor')
-      with open(gov_path, 'w') as gov:
-        gov.write('performance\n')
-      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-      pstate_dir = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate")
-      os.makedirs(pstate_dir)
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("active\n")
-
-      upowerd, obj_upower = self.spawn_server_template(
-            'upower', {'DaemonVersion': '0.99', 'OnBattery': False}, stdout=subprocess.PIPE)
-
-      self.start_daemon()
-
-      with open(gov_path, 'rb') as f:
-        contents = f.read()
-        self.assertEqual(contents, b'powersave')
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(profiles[0]['Driver'], 'amd_pstate')
-      self.assertEqual(profiles[0]['Profile'], 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      # This matches what's written by ppd-driver-amd-pstate.c
-      self.assertEqual(contents, b'balance_performance')
-
-      self.stop_daemon()
-
-      upowerd.terminate()
-      upowerd.wait()
-      upowerd.stdout.close()
-
-    def test_amd_pstate_error(self):
-      '''AMD P-State driver in error state'''
-
-      pstate_dir = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate")
-      os.makedirs(pstate_dir)
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("active\n")
-
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      with open(os.path.join(dir1, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      pref_path = os.path.join(dir1, "energy_performance_preference")
-      old_umask = os.umask(0o333)
-      with open(pref_path,'w') as prefs:
-        prefs.write("balance_performance\n")
-      os.umask(old_umask)
-      # Make file non-writable to root
-      if os.geteuid() == 0:
-        if not GLib.find_program_in_path('chattr'):
-          os._exit(77)
-        subprocess.check_output(['chattr', '+i', pref_path])
-
-      self.start_daemon()
-
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      # Error when setting performance mode
-      with self.assertRaises(gi.repository.GLib.GError):
-        self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'balance_performance\n')
-
-      self.stop_daemon()
-
-      if os.geteuid() == 0:
-        subprocess.check_output(['chattr', '-i', pref_path])
-
-    def test_amd_pstate_passive(self):
-      '''AMD P-State in passive mode -> placeholder'''
-
-      dir1 = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/")
-      os.makedirs(dir1)
-      with open(os.path.join(dir1, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-
-      # Create AMD P-State configuration
-      pstate_dir = os.path.join(self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate")
-      os.makedirs(pstate_dir)
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("passive\n")
-
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 2)
-      self.assertEqual(profiles[0]['Driver'], 'placeholder')
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'balanced')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'performance\n')
-
-      # Set performance mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-      contents = None
-      with open(os.path.join(dir1, "energy_performance_preference"), 'rb') as f:
-        contents = f.read()
-      self.assertEqual(contents, b'performance\n')
-
-      self.stop_daemon()
-
-    def test_dytc_performance_driver(self):
-      '''Lenovo DYTC performance driver'''
-
-      self.create_dytc_device()
-      self.create_platform_profile()
-      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(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
-      self.testbed.set_attribute(self.tp_acpi, 'dytc_lapmode', '1\n')
-      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('PerformanceDegraded') == '')
-
-      # Performance mode didn't change
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'performance')
-
-      # Switch to power-saver mode
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEventually(lambda: self.read_sysfs_file("sys/firmware/acpi/platform_profile") == b'low-power')
-      self.assertEqual(self.get_dbus_property('ActiveProfile'), 'power-saver')
-
-      # And mimick a user pressing a Fn+H
-      with open(os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/platform_profile"), 'w') as platform_profile:
-        platform_profile.write('performance\n')
-      self.assertEventually(lambda: self.get_dbus_property('ActiveProfile') == 'performance')
-
-    def test_fake_driver(self):
-      '''Test that the fake driver works'''
-
-      os.environ['POWER_PROFILE_DAEMON_FAKE_DRIVER'] = '1'
-      self.start_daemon()
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.stop_daemon()
-
-      del os.environ['POWER_PROFILE_DAEMON_FAKE_DRIVER']
-      self.start_daemon()
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 2)
-
-    def test_trickle_charge_system(self):
-      '''Trickle power_supply charge type'''
-
-      fastcharge = self.testbed.add_device('power_supply', 'bq24190-charger', None,
-          [ 'charge_type', 'Trickle', 'scope', 'System' ],
-          []
-      )
-
-      self.start_daemon()
-
-      self.assertIn('trickle_charge', self.get_dbus_property('Actions'))
-
-      # Verify that charge-type stays untouched
-      self.assertEqual(self.read_sysfs_attr(fastcharge, 'charge_type'), b'Trickle')
-
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEqual(self.read_sysfs_attr(fastcharge, 'charge_type'), b'Trickle')
-
-    def test_trickle_charge_mode_no_change(self):
-      '''Trickle power_supply charge type'''
-
-      fastcharge = self.testbed.add_device('power_supply', 'MFi Fastcharge', None,
-          [ 'charge_type', 'Fast', 'scope', 'Device' ],
-          []
-      )
-
-      mtime = self.get_mtime(fastcharge, 'charge_type')
-      self.start_daemon()
-
-      self.assertIn('trickle_charge', self.get_dbus_property('Actions'))
-
-      # Verify that charge-type didn't get touched
-      self.assertEqual(self.read_sysfs_attr(fastcharge, 'charge_type'), b'Fast')
-      self.assertEqual(self.get_mtime(fastcharge, 'charge_type'), mtime)
-
-    def test_trickle_charge_mode(self):
-      '''Trickle power_supply charge type'''
-
-      idevice = self.testbed.add_device('usb', 'iDevice', None,
-          [],
-          [ 'ID_MODEL', 'iDevice', 'DRIVER', 'apple-mfi-fastcharge' ]
-      )
-      fastcharge = self.testbed.add_device('power_supply', 'MFi Fastcharge', idevice,
-          [ 'charge_type', 'Trickle', 'scope', 'Device' ],
-          []
-      )
-
-      self.start_daemon()
-
-      self.assertIn('trickle_charge', self.get_dbus_property('Actions'))
-
-      # Verify that charge-type got changed to Fast on startup
-      self.assertEqual(self.read_sysfs_attr(fastcharge, 'charge_type'), b'Fast')
-
-      # Verify that charge-type got changed to Trickle when power saving
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('power-saver'))
-      self.assertEqual(self.read_sysfs_attr(fastcharge, 'charge_type'), b'Trickle')
-
-      # FIXME no performance mode
-      # Verify that charge-type got changed to Fast in a non-default, non-power save mode
-      # self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      # self.assertEqual(self.read_sysfs_attr(fastcharge, 'charge_type'), 'Fast')
-
-    def test_platform_driver_late_load(self):
-      '''Test that we can handle the platform_profile driver getting loaded late'''
-      self.create_empty_platform_profile()
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 2)
-
-      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")
-
-      # Wait for profiles to get reloaded
-      self.assertEventually(lambda: len(self.get_dbus_property('Profiles')) == 3)
-      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'), 'balanced')
-      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), '')
-
-      self.stop_daemon()
-
-    def test_hp_wmi(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("cool balanced 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'cool')
-      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')
-
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
-      self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('balanced'))
-      self.assertEqual(self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b'balanced')
-
-      self.stop_daemon()
-
-    def test_quiet(self):
-      # Uses quiet 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("quiet\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')
-      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()
-
-    def test_intel_pstate_noturbo(self):
-      '''Intel P-State driver (balance)'''
-
-      # 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, 'scaling_governor'), 'w') as gov:
-        gov.write('powersave\n')
-      with open(os.path.join(dir1, "energy_performance_preference"),'w') as prefs:
-        prefs.write("performance\n")
-
-      # Create Intel P-State configuration
-      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:
-        no_turbo.write("1\n")
-      with open(os.path.join(pstate_dir, "turbo_pct"),'w') as no_turbo:
-        no_turbo.write("0\n")
-      with open(os.path.join(pstate_dir, "status"),'w') as status:
-        status.write("active\n")
-
-      self.start_daemon()
-
-      profiles = self.get_dbus_property('Profiles')
-      self.assertEqual(len(profiles), 3)
-      self.assertEqual(self.get_dbus_property('PerformanceDegraded'), '')
-
-      self.stop_daemon()
-
-    def test_powerprofilesctl_error(self):
-      '''Check that powerprofilesctl returns 1 rather than an exception on error'''
-
-      builddir = os.getenv('top_builddir', '.')
-      tool_path = os.path.join(builddir, 'src', 'powerprofilesctl')
-
-      with self.assertRaises(subprocess.CalledProcessError) as cm:
-          subprocess.check_output([tool_path, 'list'],
-                                  stderr=subprocess.PIPE,
-                                  universal_newlines=True)
-      self.assertNotIn('Traceback', cm.exception.stderr)
-
-      with self.assertRaises(subprocess.CalledProcessError) as cm:
-          subprocess.check_output([tool_path, 'get'],
-                                  stderr=subprocess.PIPE,
-                                  universal_newlines=True)
-      self.assertNotIn('Traceback', cm.exception.stderr)
-
-      with self.assertRaises(subprocess.CalledProcessError) as cm:
-          subprocess.check_output([tool_path, 'set', 'not-a-profile'],
-                                  stderr=subprocess.PIPE,
-                                  universal_newlines=True)
-      self.assertNotIn('Traceback', cm.exception.stderr)
-
-      with self.assertRaises(subprocess.CalledProcessError) as cm:
-          subprocess.check_output([tool_path, 'list-holds'],
-                                  stderr=subprocess.PIPE,
-                                  universal_newlines=True)
-      self.assertNotIn('Traceback', cm.exception.stderr)
-
-      with self.assertRaises(subprocess.CalledProcessError) as cm:
-          subprocess.check_output([tool_path, 'launch', '-p', 'power-saver', 'sleep', '1'],
-                                  stderr=subprocess.PIPE,
-                                  universal_newlines=True)
-      self.assertNotIn('Traceback', cm.exception.stderr)
-
-      self.start_daemon()
-      with self.assertRaises(subprocess.CalledProcessError) as cm:
-          subprocess.check_output([tool_path, 'set', 'not-a-profile'],
-                                  stderr=subprocess.PIPE,
-                                  universal_newlines=True)
-      self.assertNotIn('Traceback', cm.exception.stderr)
-      self.stop_daemon()
-
-    #
-    # Helper methods
-    #
-
-    @classmethod
-    def _props_to_str(cls, properties):
-        '''Convert a properties dictionary to uevent text representation.'''
-
-        prop_str = ''
-        if properties:
-            for k, v in properties.items():
-                prop_str += '%s=%s\n' % (k, v)
-        return prop_str
-
-if __name__ == '__main__':
-    # run ourselves under umockdev
-    if 'umockdev' not in os.environ.get('LD_PRELOAD', ''):
-        os.execvp('umockdev-wrapper', ['umockdev-wrapper'] + sys.argv)
-
-    unittest.main()
diff --git a/tests/integration_test.py b/tests/integration_test.py
new file mode 100644
index 0000000..f31d6c5
--- /dev/null
+++ b/tests/integration_test.py
@@ -0,0 +1,1993 @@
+#!/usr/bin/python3
+
+# power-profiles-daemon integration test suite
+#
+# Run in built tree to test local built binaries, or from anywhere else to test
+# system installed binaries.
+#
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+
+import dbus
+
+try:
+    import gi
+    from gi.repository import GLib
+    from gi.repository import Gio
+except ImportError as e:
+    sys.stderr.write(
+        f"Skipping tests, PyGobject not available for Python 3, or missing GI typelibs: {str(e)}\n"
+    )
+    sys.exit(77)
+
+try:
+    gi.require_version("UMockdev", "1.0")
+    from gi.repository import UMockdev
+except ImportError:
+    sys.stderr.write("Skipping tests, umockdev not available.\n")
+    sys.stderr.write("(https://github.com/martinpitt/umockdev)\n")
+    sys.exit(77)
+
+try:
+    import dbusmock
+except ImportError:
+    sys.stderr.write("Skipping tests, python-dbusmock not available.\n")
+    sys.stderr.write("(http://pypi.python.org/pypi/python-dbusmock)")
+    sys.exit(77)
+
+
+# pylint: disable=too-many-public-methods,too-many-instance-attributes
+class Tests(dbusmock.DBusTestCase):
+    """Dbus based integration unit tests"""
+
+    PP = "org.freedesktop.UPower.PowerProfiles"
+    PP_PATH = "/org/freedesktop/UPower/PowerProfiles"
+    PP_INTERFACE = "org.freedesktop.UPower.PowerProfiles"
+
+    @classmethod
+    def setUpClass(cls):
+        # run from local build tree if we are in one, otherwise use system instance
+        builddir = os.getenv("top_builddir", ".")
+        if os.access(os.path.join(builddir, "src", "power-profiles-daemon"), os.X_OK):
+            cls.daemon_path = os.path.join(builddir, "src", "power-profiles-daemon")
+            print(f"Testing binaries from local build tree {cls.daemon_path}")
+        elif os.environ.get("UNDER_JHBUILD", False):
+            jhbuild_prefix = os.environ["JHBUILD_PREFIX"]
+            cls.daemon_path = os.path.join(
+                jhbuild_prefix, "libexec", "power-profiles-daemon"
+            )
+            print(f"Testing binaries from JHBuild {cls.daemon_path}")
+        else:
+            cls.daemon_path = None
+            with open(
+                "/usr/lib/systemd/system/power-profiles-daemon.service",
+                encoding="utf-8",
+            ) as tmpf:
+                for line in tmpf:
+                    if line.startswith("ExecStart="):
+                        cls.daemon_path = line.split("=", 1)[1].strip()
+                        break
+            assert (
+                cls.daemon_path
+            ), "could not determine daemon path from systemd .service file"
+            print(f"Testing installed system binary {cls.daemon_path}")
+
+        # fail on CRITICALs on client and server side
+        GLib.log_set_always_fatal(
+            GLib.LogLevelFlags.LEVEL_WARNING
+            | GLib.LogLevelFlags.LEVEL_ERROR
+            | GLib.LogLevelFlags.LEVEL_CRITICAL
+        )
+        os.environ["G_DEBUG"] = "fatal_warnings"
+
+        # set up a fake system D-BUS
+        cls.test_bus = Gio.TestDBus.new(Gio.TestDBusFlags.NONE)
+        cls.test_bus.up()
+        try:
+            del os.environ["DBUS_SESSION_BUS_ADDRESS"]
+        except KeyError:
+            pass
+        os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = cls.test_bus.get_bus_address()
+
+        cls.dbus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
+        cls.dbus_con = cls.get_dbus(True)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.test_bus.down()
+        dbusmock.DBusTestCase.tearDownClass()
+
+    def setUp(self):
+        """Set up a local umockdev testbed.
+
+        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(
+            [
+                "org.freedesktop.UPower.PowerProfiles.switch-profile",
+                "org.freedesktop.UPower.PowerProfiles.hold-profile",
+            ]
+        )
+
+        self.proxy = None
+        self.props_proxy = None
+        self.log = None
+        self.daemon = None
+        self.changed_properties = {}
+
+        # Used for dytc devices
+        self.tp_acpi = None
+
+    def run(self, result=None):
+        super().run(result)
+        if not result or not self.log:
+            return
+        if len(result.errors) + len(result.failures) or os.getenv("PPD_TEST_VERBOSE"):
+            with open(self.log.name, encoding="utf-8") as tmpf:
+                sys.stderr.write("\n-------------- daemon log: ----------------\n")
+                sys.stderr.write(tmpf.read())
+                sys.stderr.write("------------------------------\n")
+
+    def tearDown(self):
+        del self.testbed
+        self.stop_daemon()
+
+        if self.polkitd:
+            self.polkitd.stdout.close()
+            try:
+                self.polkitd.kill()
+            except OSError:
+                pass
+            self.polkitd.wait()
+
+        self.obj_polkit = None
+
+        del self.tp_acpi
+        try:
+            os.remove(self.testbed.get_root_dir() + "/" + "ppd_test_conf.ini")
+        except AttributeError:
+            pass
+
+    #
+    # Daemon control and D-BUS I/O
+    #
+
+    def start_daemon(self):
+        """Start daemon and create DBus proxy.
+
+        When done, this sets self.proxy as the Gio.DBusProxy for power-profiles-daemon.
+        """
+        env = os.environ.copy()
+        env["G_DEBUG"] = "fatal-criticals"
+        env["G_MESSAGES_DEBUG"] = "all"
+        # note: Python doesn't propagate the setenv from Testbed.new(), so we
+        # have to do that ourselves
+        env["UMOCKDEV_DIR"] = self.testbed.get_root_dir()
+        env["LD_PRELOAD"] = os.getenv("PPD_LD_PRELOAD") + " " + os.getenv("LD_PRELOAD")
+        self.log = tempfile.NamedTemporaryFile()  # pylint: disable=consider-using-with
+        daemon_path = [self.daemon_path, "-v"]
+        if os.getenv("PPD_TEST_WRAPPER"):
+            daemon_path = os.getenv("PPD_TEST_WRAPPER").split(" ") + daemon_path
+        elif os.getenv("VALGRIND"):
+            daemon_path = ["valgrind"] + daemon_path
+
+        # pylint: disable=consider-using-with
+        self.daemon = subprocess.Popen(
+            daemon_path, env=env, stdout=self.log, stderr=subprocess.STDOUT
+        )
+        self.addCleanup(self.daemon.kill)
+
+        def on_proxy_connected(_, res):
+            try:
+                self.proxy = Gio.DBusProxy.new_finish(res)
+                print(f"Proxy to {self.proxy.get_name()} connected")
+            except GLib.Error as exc:
+                self.fail(exc)
+
+        cancellable = Gio.Cancellable()
+        self.addCleanup(cancellable.cancel)
+        Gio.DBusProxy.new(
+            self.dbus,
+            Gio.DBusProxyFlags.DO_NOT_AUTO_START,
+            None,
+            self.PP,
+            self.PP_PATH,
+            self.PP_INTERFACE,
+            cancellable,
+            on_proxy_connected,
+        )
+
+        # wait until the daemon gets online
+        self.assert_eventually(
+            lambda: self.proxy and self.proxy.get_name_owner(),
+            timeout=10 * 1000,
+            message="daemon did not start in 10 seconds",
+        )
+
+        def properties_changed_cb(_, changed_properties, invalidated):
+            self.changed_properties.update(changed_properties.unpack())
+
+        self.addCleanup(
+            self.proxy.disconnect,
+            self.proxy.connect("g-properties-changed", properties_changed_cb),
+        )
+
+        self.assertEqual(self.daemon.poll(), None, "daemon crashed")
+
+    def ensure_dbus_properties_proxies(self):
+        self.props_proxy = Gio.DBusProxy.new_sync(
+            self.dbus,
+            Gio.DBusProxyFlags.DO_NOT_AUTO_START
+            | Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION
+            | Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES
+            | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS,
+            None,
+            self.PP,
+            self.PP_PATH,
+            "org.freedesktop.DBus.Properties",
+            None,
+        )
+
+    def stop_daemon(self):
+        """Stop the daemon if it is running."""
+
+        if self.daemon:
+            try:
+                self.daemon.terminate()
+            except OSError:
+                pass
+            self.assertEqual(self.daemon.wait(timeout=3000), 0)
+
+        self.daemon = None
+        self.proxy = None
+
+    def get_dbus_property(self, name):
+        """Get property value from daemon D-Bus interface."""
+        self.ensure_dbus_properties_proxies()
+        return self.props_proxy.Get("(ss)", self.PP, name)
+
+    def set_dbus_property(self, name, value):
+        """Set property value on daemon D-Bus interface."""
+        self.ensure_dbus_properties_proxies()
+        return self.props_proxy.Set("(ssv)", self.PP, name, value)
+
+    def call_dbus_method(self, name, parameters):
+        """Call a method of the daemon D-Bus interface."""
+        return self.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
+
+    def count_text_in_log(self, text):
+        with open(self.log.name, encoding="utf-8") as tmpf:
+            return tmpf.read().count(text)
+
+    def read_file_contents(self, path):
+        """Get the contents of a file"""
+        with open(path, "rb") as tmpf:
+            return tmpf.read()
+
+    def read_sysfs_file(self, path):
+        return self.read_file_contents(
+            self.testbed.get_root_dir() + "/" + path
+        ).rstrip()
+
+    def read_sysfs_attr(self, device, attribute):
+        return self.read_sysfs_file(device + "/" + attribute)
+
+    def get_mtime(self, device, attribute):
+        return os.path.getmtime(
+            self.testbed.get_root_dir() + "/" + device + "/" + attribute
+        )
+
+    def write_file_contents(self, path, contents):
+        """Set the contents of a file"""
+        with open(path, "wb") as tmpf:
+            return tmpf.write(
+                contents if isinstance(contents, bytes) else contents.encode("utf-8")
+            )
+
+    def change_immutable(self, fname, enable):
+        attr = "-"
+        if enable:
+            os.chmod(fname, 0o444)
+            attr = "+"
+        if os.geteuid() == 0:
+            if not GLib.find_program_in_path("chattr"):
+                self.skipTest("chattr is not found")
+
+            subprocess.check_output(["chattr", f"{attr}i", fname])
+        if not enable:
+            os.chmod(fname, 0o666)
+
+    def create_dytc_device(self):
+        self.tp_acpi = self.testbed.add_device(
+            "platform",
+            "thinkpad_acpi",
+            None,
+            ["dytc_lapmode", "0\n"],
+            ["DEVPATH", "/devices/platform/thinkpad_acpi"],
+        )
+
+    def create_amd_apu(self):
+        proc_dir = os.path.join(self.testbed.get_root_dir(), "proc/")
+        os.makedirs(proc_dir)
+        self.write_file_contents(
+            os.path.join(proc_dir, "cpuinfo"), "vendor_id	: AuthenticAMD\n"
+        )
+
+    def create_empty_platform_profile(self):
+        acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(acpi_dir)
+        self.write_file_contents(os.path.join(acpi_dir, "platform_profile"), "\n")
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile_choices"), "\n"
+        )
+
+    def create_platform_profile(self):
+        acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(acpi_dir, exist_ok=True)
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile"), "performance\n"
+        )
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile_choices"),
+            "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 assert_eventually(self, condition, message=None, timeout=5000):
+        """Assert that condition function eventually returns True.
+
+        Timeout is in milliseconds, defaulting to 5000 (5 seconds). message is
+        printed on failure.
+        """
+        if condition():
+            return
+
+        done = False
+
+        def on_timeout_reached():
+            nonlocal done
+            done = True
+
+        source = GLib.timeout_add(timeout, on_timeout_reached)
+        while not done:
+            if condition():
+                GLib.source_remove(source)
+                return
+            GLib.MainContext.default().iteration(False)
+
+        self.fail(message or "timed out waiting for " + str(condition))
+
+    def assert_file_eventually_contains(self, path, contents, timeout=800):
+        """Asserts that file contents eventually matches expectations"""
+        encoded = contents.encode("utf-8")
+        return self.assert_eventually(
+            lambda: self.read_file_contents(path) == encoded,
+            timeout=timeout,
+            message=f"file '{path}' does not contain '{contents}', "
+            + "but '{self.read_file_contents(path)}'",
+        )
+
+    def assert_sysfs_attr_eventually_is(self, device, attribute, contents, timeout=800):
+        """Asserts that file contents eventually matches expectations"""
+        encoded = contents.encode("utf-8")
+        return self.assert_eventually(
+            lambda: self.read_sysfs_attr(device, attribute) == encoded,
+            timeout=timeout,
+            message=f"file {device} '{attribute}' does not contain '{contents}', "
+            + "but '{self.read_sysfs_attr(device, attribute)}'",
+        )
+
+    def assert_dbus_property_eventually_is(self, prop, value, timeout=1200):
+        """Asserts that a dbus property eventually is what expected"""
+        return self.assert_eventually(
+            lambda: self.get_dbus_property(prop) == value,
+            timeout=timeout,
+            message=f"property '{prop}' is not '{value}', but '{self.get_dbus_property(prop)}'",
+        )
+
+    #
+    # Actual test cases
+    #
+    def test_dbus_startup_error(self):
+        """D-Bus startup error"""
+
+        self.start_daemon()
+        daemon_path = [self.daemon_path]
+        if os.getenv("PPD_TEST_WRAPPER"):
+            daemon_path = os.getenv("PPD_TEST_WRAPPER").split(" ") + daemon_path
+        out = subprocess.run(
+            daemon_path,
+            env={
+                "LD_PRELOAD": os.getenv("PPD_LD_PRELOAD")
+                + " "
+                + os.getenv("LD_PRELOAD")
+            },
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            check=False,
+        )
+        self.assertEqual(
+            out.returncode, 1, "power-profile-daemon started but should have failed"
+        )
+        self.stop_daemon()
+
+    def test_no_performance_driver(self):
+        """no performance driver"""
+
+        self.start_daemon()
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+        self.assertEqual(self.get_dbus_property("PerformanceDegraded"), "")
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+        self.assertEqual(profiles[1]["Driver"], "placeholder")
+        self.assertEqual(profiles[1]["PlatformDriver"], "placeholder")
+        self.assertEqual(profiles[0]["PlatformDriver"], "placeholder")
+        self.assertEqual(profiles[1]["Profile"], "balanced")
+        self.assertEqual(profiles[0]["Profile"], "power-saver")
+
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        with self.assertRaises(gi.repository.GLib.GError):
+            self.set_dbus_property(
+                "ActiveProfile", GLib.Variant.new_string("performance")
+            )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        with self.assertRaises(gi.repository.GLib.GError):
+            cookie = self.call_dbus_method(
+                "HoldProfile",
+                GLib.Variant("(sss)", ("performance", "testReason", "testApplication")),
+            )
+            assert cookie
+
+        self.stop_daemon()
+
+    def test_inhibited_property(self):
+        """Test that the inhibited property exists"""
+
+        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("PerformanceInhibited"), "")
+
+    def test_multi_degredation(self):
+        """Test handling of degradation from multiple drivers"""
+        self.create_dytc_device()
+        self.create_platform_profile()
+
+        # Create CPU with preference
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create Intel P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "0\n")
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        self.start_daemon()
+
+        # Set performance mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("performance"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        # Degraded CPU
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "1\n")
+        self.assert_eventually(
+            lambda: self.have_text_in_log("File monitor change happened for ")
+        )
+
+        self.assertEqual(
+            self.get_dbus_property("PerformanceDegraded"), "high-operating-temperature"
+        )
+
+        # Degraded DYTC
+        self.testbed.set_attribute(self.tp_acpi, "dytc_lapmode", "1\n")
+        self.assert_eventually(lambda: self.have_text_in_log("dytc_lapmode is now on"))
+        self.assertEqual(
+            self.get_dbus_property("PerformanceDegraded"),
+            "high-operating-temperature,lap-detected",
+        )
+
+    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("performance"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        # Degraded
+        self.testbed.set_attribute(self.tp_acpi, "dytc_lapmode", "1\n")
+        self.assert_eventually(lambda: self.have_text_in_log("dytc_lapmode is now on"))
+        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"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+    def test_intel_pstate(self):
+        """Intel P-State driver (no UPower)"""
+
+        # Create 2 CPUs with preferences
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+        dir2 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy1/"
+        )
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir2, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create Intel P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "0\n")
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "intel_pstate")
+        self.assertEqual(profiles[0]["Profile"], "power-saver")
+
+        energy_prefs = os.path.join(dir2, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "balance_performance")
+
+        # Set performance mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("performance"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        self.assert_file_eventually_contains(energy_prefs, "performance")
+
+        # Disable turbo
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "1\n")
+
+        self.assert_eventually(
+            lambda: self.have_text_in_log("File monitor change happened for ")
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        self.assertEqual(
+            self.get_dbus_property("PerformanceDegraded"), "high-operating-temperature"
+        )
+
+        self.stop_daemon()
+
+        # Verify that Lenovo DYTC and Intel P-State drivers are loaded
+        self.create_platform_profile()
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "intel_pstate")
+        self.assertEqual(profiles[0]["PlatformDriver"], "platform_profile")
+
+    def test_intel_pstate_balance(self):
+        """Intel P-State driver (balance)"""
+
+        # Create CPU with preference
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        gov_path = os.path.join(dir1, "scaling_governor")
+        self.write_file_contents(gov_path, "performance\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        upowerd, obj_upower = self.spawn_server_template(
+            "upower",
+            {"DaemonVersion": "0.99", "OnBattery": False},
+            stdout=subprocess.PIPE,
+        )
+        self.assertNotEqual(upowerd, None)
+        self.assertNotEqual(obj_upower, None)
+
+        self.start_daemon()
+
+        self.assert_file_eventually_contains(gov_path, "powersave")
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "intel_pstate")
+        self.assertEqual(profiles[0]["Profile"], "power-saver")
+
+        self.assert_file_eventually_contains(
+            os.path.join(dir1, "energy_performance_preference"), "balance_performance"
+        )
+
+        self.stop_daemon()
+
+        upowerd.terminate()
+        upowerd.wait()
+        upowerd.stdout.close()
+
+    def test_intel_pstate_error(self):
+        """Intel P-State driver in error state"""
+
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        pref_path = os.path.join(dir1, "energy_performance_preference")
+        old_umask = os.umask(0o333)
+        self.write_file_contents(pref_path, "balance_performance\n")
+        os.umask(old_umask)
+        # Make file non-writable to root
+        self.change_immutable(pref_path, True)
+
+        self.start_daemon()
+
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # Error when setting performance mode
+        with self.assertRaises(gi.repository.GLib.GError):
+            self.set_dbus_property(
+                "ActiveProfile", GLib.Variant.new_string("performance")
+            )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        energy_prefs = os.path.join(dir1, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "balance_performance\n")
+
+        self.stop_daemon()
+
+        self.change_immutable(pref_path, False)
+
+    def test_intel_pstate_passive(self):
+        """Intel P-State in passive mode -> placeholder"""
+
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create Intel P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "0\n")
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "passive\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+        self.assertEqual(profiles[0]["Driver"], "placeholder")
+        self.assertEqual(profiles[0]["PlatformDriver"], "placeholder")
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        energy_prefs = os.path.join(dir1, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "performance\n")
+
+        # Set performance mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        energy_prefs = os.path.join(dir1, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "performance\n")
+
+        self.stop_daemon()
+
+    def test_intel_pstate_passive_with_epb(self):
+        """Intel P-State in passive mode (no HWP) with energy_perf_bias"""
+
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+        dir2 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpu0/power/"
+        )
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "energy_perf_bias"), "6")
+
+        # Create Intel P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "0\n")
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "passive\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "intel_pstate")
+        self.assertEqual(profiles[0]["PlatformDriver"], "placeholder")
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # Set power-saver mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        energy_perf_bias = os.path.join(dir2, "energy_perf_bias")
+        self.assert_file_eventually_contains(energy_perf_bias, "15")
+
+        # Set performance mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("performance"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        self.assert_file_eventually_contains(energy_perf_bias, "0")
+
+        self.stop_daemon()
+
+    def test_action_blocklist(self):
+        """Test action blocklist works"""
+        self.testbed.add_device(
+            "drm",
+            "card1-eDP",
+            None,
+            ["amdgpu/panel_power_savings", "0"],
+            ["DEVTYPE", "drm_connector"],
+        )
+
+        self.create_amd_apu()
+
+        self.spawn_server_template(
+            "upower",
+            {"DaemonVersion": "0.99", "OnBattery": False},
+            stdout=subprocess.PIPE,
+        )
+
+        # Block panel_power action
+        os.environ["POWER_PROFILE_DAEMON_ACTION_BLOCK"] = "amdgpu_panel_power"
+        self.start_daemon()
+        self.assertNotIn("amdgpu_panel_power", self.get_dbus_property("Actions"))
+
+    def test_driver_blocklist(self):
+        """Test driver blocklist works"""
+        # Create 2 CPUs with preferences
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        scaling_governor = os.path.join(dir1, "scaling_governor")
+        self.write_file_contents(scaling_governor, "powersave\n")
+
+        prefs1 = os.path.join(dir1, "energy_performance_preference")
+        self.write_file_contents(prefs1, "performance\n")
+
+        dir2 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy1/"
+        )
+        os.makedirs(dir2)
+        scaling_governor = os.path.join(dir2, "scaling_governor")
+        self.write_file_contents(scaling_governor, "powersave\n")
+        prefs2 = os.path.join(
+            dir2,
+            "energy_performance_preference",
+        )
+        self.write_file_contents(prefs2, "prformance\n")
+
+        # Create AMD P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        # create ACPI platform profile
+        self.create_platform_profile()
+        profile = os.path.join(
+            self.testbed.get_root_dir(), "sys/firmware/acpi/platform_profile"
+        )
+        self.assertNotEqual(profile, None)
+
+        # desktop PM profile
+        dir3 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir3, exist_ok=True)
+        self.write_file_contents(os.path.join(dir3, "pm_profile"), "1\n")
+
+        # block platform profile
+        os.environ["POWER_PROFILE_DAEMON_DRIVER_BLOCK"] = "platform_profile"
+
+        # Verify that only amd-pstate is loaded
+        self.start_daemon()
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "amd_pstate")
+        self.assertEqual(profiles[0]["PlatformDriver"], "placeholder")
+
+        self.stop_daemon()
+
+        # block both drivers
+        os.environ["POWER_PROFILE_DAEMON_DRIVER_BLOCK"] = "amd_pstate,platform_profile"
+
+        # Verify that only placeholder is loaded
+        self.start_daemon()
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+        self.assertEqual(profiles[0]["PlatformDriver"], "placeholder")
+
+    # pylint: disable=too-many-statements
+    def test_multi_driver_flows(self):
+        """Test corner cases associated with multiple drivers"""
+
+        # Create 2 CPUs with preferences
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        prefs1 = os.path.join(dir1, "energy_performance_preference")
+        self.write_file_contents(prefs1, "performance\n")
+
+        dir2 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy1/"
+        )
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "scaling_governor"), "powersave\n")
+        prefs2 = os.path.join(dir2, "energy_performance_preference")
+        self.write_file_contents(prefs2, "performance\n")
+
+        # Create AMD P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        # create ACPI platform profile
+        self.create_platform_profile()
+        profile = os.path.join(
+            self.testbed.get_root_dir(), "sys/firmware/acpi/platform_profile"
+        )
+
+        # desktop PM profile
+        dir3 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir3, exist_ok=True)
+        self.write_file_contents(os.path.join(dir3, "pm_profile"), "1\n")
+
+        self.start_daemon()
+
+        # Verify that both drivers are loaded
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "amd_pstate")
+        self.assertEqual(profiles[0]["PlatformDriver"], "platform_profile")
+
+        # test both drivers can switch to power-saver
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        # test both drivers can switch to performance
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("performance"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        # test both drivers can switch to balanced
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("balanced"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # test when CPU driver fails to write
+        self.change_immutable(prefs1, True)
+        with self.assertRaises(gi.repository.GLib.GError):
+            self.set_dbus_property(
+                "ActiveProfile", GLib.Variant.new_string("power-saver")
+            )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+        self.assertEqual(
+            self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b"balanced"
+        )
+        self.change_immutable(prefs1, False)
+
+        # test when platform driver fails to write
+        self.change_immutable(profile, True)
+        with self.assertRaises(gi.repository.GLib.GError):
+            self.set_dbus_property(
+                "ActiveProfile", GLib.Variant.new_string("power-saver")
+            )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # make sure CPU was undone since platform failed
+        self.assertEqual(
+            self.read_sysfs_file(
+                "sys/devices/system/cpu/cpufreq/policy0/energy_performance_preference"
+            ),
+            b"balance_performance",
+        )
+        self.assertEqual(
+            self.read_sysfs_file(
+                "sys/devices/system/cpu/cpufreq/policy1/energy_performance_preference"
+            ),
+            b"balance_performance",
+        )
+        self.change_immutable(profile, False)
+
+        self.stop_daemon()
+
+    # pylint: disable=too-many-statements
+    def test_amd_pstate(self):
+        """AMD P-State driver (no UPower)"""
+
+        # Create 2 CPUs with preferences
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+        dir2 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy1/"
+        )
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir2, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create AMD P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        # desktop PM profile
+        dir3 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir3)
+        self.write_file_contents(os.path.join(dir3, "pm_profile"), "1\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "amd_pstate")
+        self.assertEqual(profiles[0]["Profile"], "power-saver")
+
+        energy_prefs = os.path.join(dir2, "energy_performance_preference")
+        scaling_governor = os.path.join(dir2, "scaling_governor")
+
+        self.assert_file_eventually_contains(energy_prefs, "balance_performance")
+        self.assert_file_eventually_contains(scaling_governor, "powersave")
+
+        # Set performance mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("performance"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        self.assert_file_eventually_contains(energy_prefs, "performance")
+        self.assert_file_eventually_contains(scaling_governor, "performance")
+
+        # Set powersave mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        self.assert_file_eventually_contains(energy_prefs, "power")
+        self.assert_file_eventually_contains(scaling_governor, "powersave")
+
+        self.stop_daemon()
+
+    def test_amd_pstate_balance(self):
+        """AMD P-State driver (balance)"""
+
+        # Create CPU with preference
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        gov_path = os.path.join(dir1, "scaling_governor")
+        self.write_file_contents(gov_path, "performance\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        # desktop PM profile
+        dir2 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "pm_profile"), "1\n")
+
+        upowerd, obj_upower = self.spawn_server_template(
+            "upower",
+            {"DaemonVersion": "0.99", "OnBattery": False},
+            stdout=subprocess.PIPE,
+        )
+        self.assertTrue(upowerd)
+        self.assertTrue(obj_upower)
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(profiles[0]["Driver"], "multiple")
+        self.assertEqual(profiles[0]["CpuDriver"], "amd_pstate")
+        self.assertEqual(profiles[0]["Profile"], "power-saver")
+
+        # This matches what's written by ppd-driver-amd-pstate.c
+        energy_prefs = os.path.join(dir1, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "balance_performance")
+
+        scaling_governor = os.path.join(dir1, "scaling_governor")
+        self.assert_file_eventually_contains(scaling_governor, "powersave")
+
+        self.stop_daemon()
+
+        upowerd.terminate()
+        upowerd.wait()
+        upowerd.stdout.close()
+
+    def test_amd_pstate_error(self):
+        """AMD P-State driver in error state"""
+
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        pref_path = os.path.join(dir1, "energy_performance_preference")
+        old_umask = os.umask(0o333)
+        self.write_file_contents(pref_path, "balance_performance\n")
+        os.umask(old_umask)
+        # Make file non-writable to root
+        self.change_immutable(pref_path, True)
+
+        # desktop PM profile
+        dir2 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "pm_profile"), "1\n")
+
+        self.start_daemon()
+
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # Error when setting performance mode
+        with self.assertRaises(gi.repository.GLib.GError):
+            self.set_dbus_property(
+                "ActiveProfile", GLib.Variant.new_string("performance")
+            )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        energy_prefs = os.path.join(dir1, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "balance_performance\n")
+
+        self.stop_daemon()
+
+        self.change_immutable(pref_path, False)
+
+    def test_amd_pstate_passive(self):
+        """AMD P-State in passive mode -> placeholder"""
+
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create AMD P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "passive\n")
+
+        # desktop PM profile
+        dir2 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "pm_profile"), "1\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+        self.assertEqual(profiles[0]["Driver"], "placeholder")
+        self.assertEqual(profiles[0]["PlatformDriver"], "placeholder")
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        energy_prefs = os.path.join(dir1, "energy_performance_preference")
+        self.assert_file_eventually_contains(energy_prefs, "performance\n")
+
+        # Set performance mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        self.assert_file_eventually_contains(energy_prefs, "performance\n")
+
+        self.stop_daemon()
+
+    def test_amd_pstate_server(self):
+        # Create 2 CPUs with preferences
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+        dir2 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy1/"
+        )
+        os.makedirs(dir2)
+        self.write_file_contents(os.path.join(dir2, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir2, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create AMD P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/amd_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        # server PM profile
+        dir3 = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(dir3)
+        self.write_file_contents(os.path.join(dir3, "pm_profile"), "4\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+        with self.assertRaises(KeyError):
+            print(profiles[0]["CpuDriver"])
+
+        self.stop_daemon()
+
+    def test_dytc_performance_driver(self):
+        """Lenovo DYTC performance driver"""
+
+        self.create_dytc_device()
+        self.create_platform_profile()
+        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]["PlatformDriver"], "platform_profile")
+        self.assertEqual(profiles[0]["Profile"], "power-saver")
+        self.assertEqual(profiles[2]["PlatformDriver"], "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
+        self.testbed.set_attribute(self.tp_acpi, "dytc_lapmode", "1\n")
+        self.assert_dbus_property_eventually_is("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.assert_dbus_property_eventually_is("PerformanceDegraded", "")
+
+        # Performance mode didn't change
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        # Switch to power-saver mode
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assert_eventually(
+            lambda: self.read_sysfs_file("sys/firmware/acpi/platform_profile")
+            == b"low-power"
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        # And mimic a user pressing a Fn+H
+        platform_profile = os.path.join(
+            self.testbed.get_root_dir(), "sys/firmware/acpi/platform_profile"
+        )
+        self.write_file_contents(platform_profile, "performance\n")
+        self.assert_dbus_property_eventually_is("ActiveProfile", "performance")
+
+    def test_fake_driver(self):
+        """Test that the fake driver works"""
+
+        os.environ["POWER_PROFILE_DAEMON_FAKE_DRIVER"] = "1"
+        self.start_daemon()
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.stop_daemon()
+
+        del os.environ["POWER_PROFILE_DAEMON_FAKE_DRIVER"]
+        self.start_daemon()
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+
+    def test_amdgpu_panel_power(self):
+        """Verify AMDGPU Panel power actions"""
+        amdgpu_panel_power_savings = "amdgpu/panel_power_savings"
+        edp = self.testbed.add_device(
+            "drm",
+            "card1-eDP",
+            None,
+            ["status", "connected\n", amdgpu_panel_power_savings, "0"],
+            ["DEVTYPE", "drm_connector"],
+        )
+
+        self.create_amd_apu()
+
+        self.start_daemon()
+
+        self.assertIn("amdgpu_panel_power", self.get_dbus_property("Actions"))
+
+        # verify it hasn't been updated yet due to missing upower
+        self.assert_sysfs_attr_eventually_is(edp, amdgpu_panel_power_savings, "0")
+
+        # start upower and try again
+        self.stop_daemon()
+        self.spawn_server_template(
+            "upower",
+            {"DaemonVersion": "0.99", "OnBattery": True},
+            stdout=subprocess.PIPE,
+        )
+        self.start_daemon()
+
+        # verify balanced updated it
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("balanced"))
+        self.assert_sysfs_attr_eventually_is(edp, amdgpu_panel_power_savings, "1")
+
+        # verify power saver updated it
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assert_sysfs_attr_eventually_is(edp, amdgpu_panel_power_savings, "3")
+
+        # add another device that supports the feature
+        edp2 = self.testbed.add_device(
+            "drm",
+            "card2-eDP",
+            None,
+            ["status", "connected\n", amdgpu_panel_power_savings, "0"],
+            ["DEVTYPE", "drm_connector"],
+        )
+
+        # verify power saver got updated for it
+        self.assert_sysfs_attr_eventually_is(edp2, amdgpu_panel_power_savings, "3")
+
+        # add another device that supports the feature, but panel is disconnected
+        edp3 = self.testbed.add_device(
+            "drm",
+            "card3-eDP",
+            None,
+            ["status", "disconnected\n", amdgpu_panel_power_savings, "0"],
+            ["DEVTYPE", "drm_connector"],
+        )
+
+        # verify power saver didn't get updated for it
+        self.assert_sysfs_attr_eventually_is(edp3, amdgpu_panel_power_savings, "0")
+
+    def test_trickle_charge_system(self):
+        """Trickle power_supply charge type"""
+
+        fastcharge = self.testbed.add_device(
+            "power_supply",
+            "bq24190-charger",
+            None,
+            ["charge_type", "Trickle", "scope", "System"],
+            [],
+        )
+
+        self.start_daemon()
+
+        self.assertIn("trickle_charge", self.get_dbus_property("Actions"))
+
+        # Verify that charge-type stays untouched
+        self.assertEqual(self.read_sysfs_attr(fastcharge, "charge_type"), b"Trickle")
+
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assertEqual(self.read_sysfs_attr(fastcharge, "charge_type"), b"Trickle")
+
+    def test_trickle_charge_mode_no_change(self):
+        """Trickle power_supply charge type"""
+
+        fastcharge = self.testbed.add_device(
+            "power_supply",
+            "MFi Fastcharge",
+            None,
+            ["charge_type", "Fast", "scope", "Device"],
+            [],
+        )
+
+        mtime = self.get_mtime(fastcharge, "charge_type")
+        self.start_daemon()
+
+        self.assertIn("trickle_charge", self.get_dbus_property("Actions"))
+
+        # Verify that charge-type didn't get touched
+        self.assert_sysfs_attr_eventually_is(fastcharge, "charge_type", "Fast")
+        self.assertEqual(self.get_mtime(fastcharge, "charge_type"), mtime)
+
+    def test_trickle_charge_mode(self):
+        """Trickle power_supply charge type"""
+
+        idevice = self.testbed.add_device(
+            "usb",
+            "iDevice",
+            None,
+            [],
+            ["ID_MODEL", "iDevice", "DRIVER", "apple-mfi-fastcharge"],
+        )
+        fastcharge = self.testbed.add_device(
+            "power_supply",
+            "MFi Fastcharge",
+            idevice,
+            ["charge_type", "Trickle", "scope", "Device"],
+            [],
+        )
+
+        self.start_daemon()
+
+        self.assertIn("trickle_charge", self.get_dbus_property("Actions"))
+
+        # Verify that charge-type got changed to Fast on startup
+        self.assert_sysfs_attr_eventually_is(fastcharge, "charge_type", "Fast")
+
+        # Verify that charge-type got changed to Trickle when power saving
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("power-saver"))
+        self.assert_sysfs_attr_eventually_is(fastcharge, "charge_type", "Trickle")
+
+        # FIXME no performance mode
+        # Verify that charge-type got changed to Fast in a non-default, non-power save mode
+        # self.set_dbus_property('ActiveProfile', GLib.Variant.new_string('performance'))
+        # self.assert_sysfs_attr_eventually_is(fastcharge, "charge_type", "Fast")
+
+    def test_platform_driver_late_load(self):
+        """Test that we can handle the platform_profile driver getting loaded late"""
+        self.create_empty_platform_profile()
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 2)
+
+        acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile_choices"),
+            "low-power\nbalanced\nperformance\n",
+        )
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile"), "performance\n"
+        )
+
+        # Wait for profiles to get reloaded
+        self.assert_eventually(lambda: len(self.get_dbus_property("Profiles")) == 3)
+        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"), "balanced")
+        self.assertEqual(self.get_dbus_property("PerformanceDegraded"), "")
+
+        self.stop_daemon()
+
+    def test_hp_wmi(self):
+        # Uses cool instead of low-power
+        acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(acpi_dir)
+        self.write_file_contents(os.path.join(acpi_dir, "platform_profile"), "cool\n")
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile_choices"),
+            "cool balanced 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]["PlatformDriver"], "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"cool"
+        )
+        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"
+        )
+
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("performance"))
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("balanced"))
+        self.assertEqual(
+            self.read_sysfs_file("sys/firmware/acpi/platform_profile"), b"balanced"
+        )
+
+        self.stop_daemon()
+
+    def test_quiet(self):
+        # Uses quiet instead of low-power
+        acpi_dir = os.path.join(self.testbed.get_root_dir(), "sys/firmware/acpi/")
+        os.makedirs(acpi_dir)
+        self.write_file_contents(os.path.join(acpi_dir, "platform_profile"), "quiet\n")
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile_choices"),
+            "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]["PlatformDriver"], "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.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "performance"
+        )
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfileHolds")
+            == [
+                {
+                    "ApplicationId": "testApplication",
+                    "Profile": "performance",
+                    "Reason": "testReason",
+                }
+            ]
+        )
+
+        released_cookie = None
+
+        def signal_cb(_, sender, signal, params):
+            nonlocal released_cookie
+            if signal == "ProfileReleased":
+                released_cookie = params
+
+        self.addCleanup(
+            self.proxy.disconnect, self.proxy.connect("g-signal", signal_cb)
+        )
+
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        profile_holds = self.get_dbus_property("ActiveProfileHolds")
+        self.assertEqual(len(profile_holds), 1)
+        self.assertEqual(profile_holds[0]["Profile"], "performance")
+        self.assertEqual(profile_holds[0]["Reason"], "testReason")
+        self.assertEqual(profile_holds[0]["ApplicationId"], "testApplication")
+
+        self.call_dbus_method("ReleaseProfile", GLib.Variant("(u)", cookie))
+        self.assert_eventually(lambda: released_cookie == cookie)
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "balanced"
+        )
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfileHolds") == []
+        )
+        profile_holds = self.get_dbus_property("ActiveProfileHolds")
+        self.assertEqual(len(profile_holds), 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.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "performance"
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+
+        self.set_dbus_property("ActiveProfile", GLib.Variant.new_string("balanced"))
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "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.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "power-saver"
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+        cookie = self.call_dbus_method(
+            "HoldProfile", GLib.Variant("(sss)", ("performance", "", ""))
+        )
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfileHolds")
+            == [
+                {
+                    "ApplicationId": "",
+                    "Profile": "performance",
+                    "Reason": "",
+                }
+            ]
+        )
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "performance"
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        self.call_dbus_method("ReleaseProfile", GLib.Variant("(u)", cookie))
+        self.assert_eventually(lambda: released_cookie == cookie)
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfileHolds") == []
+        )
+        self.assert_eventually(
+            lambda: self.changed_properties.get("ActiveProfile") == "power-saver"
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        self.stop_daemon()
+
+    def test_vanishing_hold(self):
+        self.create_platform_profile()
+        self.start_daemon()
+
+        sourcedir = os.getenv("top_srcdir", ".")
+        tool_path = os.path.join(sourcedir, "src", "powerprofilesctl")
+
+        with subprocess.Popen(
+            [tool_path, "launch", "-p", "power-saver", "sleep", "3600"],
+            stdout=sys.stdout,
+            stderr=sys.stderr,
+        ) as launch_process:
+            self.assertTrue(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()
+            self.assertEqual(launch_process.wait(), 0)
+
+        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
+        performance_cookie = self.call_dbus_method(
+            "HoldProfile", GLib.Variant("(sss)", ("performance", "", ""))
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        powersaver_cookie = 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)", performance_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+        self.call_dbus_method("ReleaseProfile", GLib.Variant("(u)", powersaver_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # hold performance and then power-saver, but release power-saver first
+        performance_cookie = self.call_dbus_method(
+            "HoldProfile", GLib.Variant("(sss)", ("performance", "", ""))
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        powersaver_cookie = 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)", powersaver_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        self.call_dbus_method("ReleaseProfile", GLib.Variant("(u)", performance_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # hold power-saver and then performance, release in the same order
+        powersaver_cookie = self.call_dbus_method(
+            "HoldProfile", GLib.Variant("(sss)", ("power-saver", "", ""))
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+        performance_cookie = 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)", powersaver_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "performance")
+        self.call_dbus_method("ReleaseProfile", GLib.Variant("(u)", performance_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        # hold power-saver and then performance, but release performance first
+        powersaver_cookie = self.call_dbus_method(
+            "HoldProfile", GLib.Variant("(sss)", ("power-saver", "", ""))
+        )
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+        performance_cookie = 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)", performance_cookie))
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+        self.call_dbus_method("ReleaseProfile", GLib.Variant("(u)", powersaver_cookie))
+        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 tmpf:
+        #   sys.stderr.write(tmpf.read())
+        # sys.stderr.write('------------------------------\n')
+
+        self.start_daemon()
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+        # Programmatically set profile aren't saved
+        performance_cookie = self.call_dbus_method(
+            "HoldProfile", GLib.Variant("(sss)", ("performance", "", ""))
+        )
+        self.assertTrue(performance_cookie)
+        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/")
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile_choices"),
+            "low-power\nbalanced\nperformance\n",
+        )
+        self.write_file_contents(
+            os.path.join(acpi_dir, "platform_profile"), "performance\n"
+        )
+
+        self.assert_dbus_property_eventually_is("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,
+            self.PP,
+            self.PP_PATH,
+            "org.freedesktop.DBus.Properties",
+            None,
+        )
+        with self.assertRaises(gi.repository.GLib.GError) as error:
+            proxy.Set(
+                "(ssv)",
+                self.PP,
+                "ActiveProfile",
+                GLib.Variant.new_string("power-saver"),
+            )
+        self.assertIn("AccessDenied", str(error.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 error:
+            self.call_dbus_method(
+                "HoldProfile", GLib.Variant("(sss)", ("performance", "", ""))
+            )
+        self.assertIn("AccessDenied", str(error.exception))
+
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+        self.assertEqual(len(self.get_dbus_property("ActiveProfileHolds")), 0)
+
+        self.stop_daemon()
+
+    def test_get_version_prop(self):
+        """Checks that the version property is advertised"""
+        self.start_daemon()
+        self.assertTrue(self.get_dbus_property("Version"))
+
+    def test_intel_pstate_noturbo(self):
+        """Intel P-State driver (balance)"""
+
+        # Create CPU with preference
+        dir1 = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/cpufreq/policy0/"
+        )
+        os.makedirs(dir1)
+        self.write_file_contents(os.path.join(dir1, "scaling_governor"), "powersave\n")
+        self.write_file_contents(
+            os.path.join(dir1, "energy_performance_preference"), "performance\n"
+        )
+
+        # Create Intel P-State configuration
+        pstate_dir = os.path.join(
+            self.testbed.get_root_dir(), "sys/devices/system/cpu/intel_pstate"
+        )
+        os.makedirs(pstate_dir)
+        self.write_file_contents(os.path.join(pstate_dir, "no_turbo"), "1\n")
+        self.write_file_contents(os.path.join(pstate_dir, "turbo_pct"), "0\n")
+        self.write_file_contents(os.path.join(pstate_dir, "status"), "active\n")
+
+        self.start_daemon()
+
+        profiles = self.get_dbus_property("Profiles")
+        self.assertEqual(len(profiles), 3)
+        self.assertEqual(self.get_dbus_property("PerformanceDegraded"), "")
+
+        self.stop_daemon()
+
+    def test_powerprofilesctl_version_command(self):
+        """Check powerprofilesctl version command works"""
+
+        self.start_daemon()
+
+        sourcedir = os.getenv("top_srcdir", ".")
+        tool_path = os.path.join(sourcedir, "src", "powerprofilesctl")
+
+        cmd = subprocess.run([tool_path, "version"], check=True)
+        self.assertEqual(cmd.returncode, 0)
+
+    def test_powerprofilesctl_list_command(self):
+        """Check powerprofilesctl list command works"""
+
+        self.start_daemon()
+
+        sourcedir = os.getenv("top_srcdir", ".")
+        tool_path = os.path.join(sourcedir, "src", "powerprofilesctl")
+
+        cmd = subprocess.run([tool_path, "list"], capture_output=True, check=True)
+        self.assertEqual(cmd.returncode, 0)
+        self.assertIn("* balanced", cmd.stdout.decode("utf-8"))
+
+    def test_powerprofilesctl_set_get_commands(self):
+        """Check powerprofilesctl set/get command works"""
+
+        self.start_daemon()
+
+        sourcedir = os.getenv("top_srcdir", ".")
+        tool_path = os.path.join(sourcedir, "src", "powerprofilesctl")
+
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "balanced")
+
+        cmd = subprocess.run([tool_path, "get"], capture_output=True, check=True)
+        self.assertEqual(cmd.returncode, 0)
+        self.assertEqual(cmd.stdout, b"balanced\n")
+
+        cmd = subprocess.run(
+            [tool_path, "set", "power-saver"], capture_output=True, check=True
+        )
+        self.assertEqual(cmd.returncode, 0)
+
+        self.assertEqual(self.get_dbus_property("ActiveProfile"), "power-saver")
+
+        cmd = subprocess.run([tool_path, "get"], capture_output=True, check=True)
+        self.assertEqual(cmd.returncode, 0)
+        self.assertEqual(cmd.stdout, b"power-saver\n")
+
+    def test_powerprofilesctl_error(self):
+        """Check that powerprofilesctl returns 1 rather than an exception on error"""
+
+        sourcedir = os.getenv("top_srcdir", ".")
+        tool_path = os.path.join(sourcedir, "src", "powerprofilesctl")
+
+        with self.assertRaises(subprocess.CalledProcessError) as error:
+            subprocess.check_output(
+                [tool_path, "list"], stderr=subprocess.PIPE, universal_newlines=True
+            )
+        self.assertNotIn("Traceback", error.exception.stderr)
+
+        with self.assertRaises(subprocess.CalledProcessError) as error:
+            subprocess.check_output(
+                [tool_path, "get"], stderr=subprocess.PIPE, universal_newlines=True
+            )
+        self.assertNotIn("Traceback", error.exception.stderr)
+
+        with self.assertRaises(subprocess.CalledProcessError) as error:
+            subprocess.check_output(
+                [tool_path, "set", "not-a-profile"],
+                stderr=subprocess.PIPE,
+                universal_newlines=True,
+            )
+        self.assertNotIn("Traceback", error.exception.stderr)
+
+        with self.assertRaises(subprocess.CalledProcessError) as error:
+            subprocess.check_output(
+                [tool_path, "list-holds"],
+                stderr=subprocess.PIPE,
+                universal_newlines=True,
+            )
+        self.assertNotIn("Traceback", error.exception.stderr)
+
+        with self.assertRaises(subprocess.CalledProcessError) as error:
+            subprocess.check_output(
+                [tool_path, "launch", "-p", "power-saver", "sleep", "1"],
+                stderr=subprocess.PIPE,
+                universal_newlines=True,
+            )
+        self.assertNotIn("Traceback", error.exception.stderr)
+
+        self.start_daemon()
+        with self.assertRaises(subprocess.CalledProcessError) as error:
+            subprocess.check_output(
+                [tool_path, "set", "not-a-profile"],
+                stderr=subprocess.PIPE,
+                universal_newlines=True,
+            )
+        self.assertNotIn("Traceback", error.exception.stderr)
+        self.stop_daemon()
+
+    #
+    # Helper methods
+    #
+
+    @classmethod
+    def _props_to_str(cls, properties):
+        """Convert a properties dictionary to uevent text representation."""
+
+        prop_str = ""
+        if properties:
+            for key, val in properties.items():
+                prop_str += f"{key}={val}\n"
+        return prop_str
+
+
+class LegacyDBusNameTests(Tests):
+    """This will repeats all the tests in the Tests class using the legacy dbus name"""
+
+    PP = "net.hadess.PowerProfiles"
+    PP_PATH = "/net/hadess/PowerProfiles"
+    PP_INTERFACE = "net.hadess.PowerProfiles"
+
+
+if __name__ == "__main__":
+    # run ourselves under umockdev
+    if "umockdev" not in os.environ.get("LD_PRELOAD", ""):
+        os.execvp("umockdev-wrapper", ["umockdev-wrapper", sys.executable] + sys.argv)
+
+    prog = unittest.main(exit=False)
+    if prog.result.errors or prog.result.failures:
+        sys.exit(1)
+
+    # Translate to skip error
+    if prog.result.testsRun == len(prog.result.skipped):
+        sys.exit(77)
diff --git a/tests/meson.build b/tests/meson.build
index e51e90f..20e6eb3 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,18 +1,86 @@
 envs = environment()
-envs.set ('top_builddir', meson.build_root())
-envs.set ('top_srcdir', meson.source_root())
+envs.set('PPD_TEST_VERBOSE', 'true')
+envs.set ('top_builddir', meson.project_build_root())
+envs.set ('top_srcdir', meson.project_source_root())
 
-python3 = find_program('python3')
 unittest_inspector = find_program('unittest_inspector.py')
-r = run_command(unittest_inspector, files('integration-test.py'), check: true)
+integration_tests = files('integration_test.py')
+r = run_command(python3, unittest_inspector, integration_tests, check: true)
 unit_tests = r.stdout().strip().split('\n')
 
+valgrind = find_program('valgrind', required: false)
+if valgrind.found()
+    glib_share = glib_dep.get_variable('prefix') / 'share' / glib_dep.name()
+    glib_suppressions = glib_share + '/valgrind/glib.supp'
+    libfprint_wrapper = [
+        valgrind.full_path(),
+        '--tool=memcheck',
+        '--leak-check=full',
+        '--leak-resolution=high',
+        '--error-exitcode=1',
+        '--errors-for-leak-kinds=definite',
+        '--track-origins=yes',
+        '--show-leak-kinds=definite,possible',
+        '--show-error-list=yes',
+        '--gen-suppressions=all',
+        '--suppressions=' + glib_suppressions,
+    ]
+    add_test_setup('valgrind',
+        timeout_multiplier: 5,
+        env: [
+            'G_SLICE=always-malloc',
+            'UNDER_VALGRIND=1',
+            'PPD_TEST_WRAPPER=' + ' '.join(libfprint_wrapper),
+        ])
+endif
+
+preloaded_libs = []
+ppd_tests_ld_preload = []
+
+if address_sanitizer
+    # ASAN has to be the first in list
+    preloaded_libs += 'asan'
+endif
+
+foreach libname: preloaded_libs
+    lib = run_command(meson.get_compiler('c'),
+        '-print-file-name=lib at 0@.so'.format(libname),
+        check: true,
+    ).stdout().strip()
+
+    # Support linker script files
+    if run_command('grep', '-qI', '^INPUT', files(lib), check: false).returncode() == 0
+        out = run_command('cat', lib, check: true).stdout()
+        lib = out.split('(')[1].split(')')[0].strip()
+    endif
+
+    if lib != '' and lib[0] == '/'
+        message('Found library @0@ as @1@'.format(libname, lib))
+        ppd_tests_ld_preload += '@0@'.format(files(lib)[0])
+    else
+        tests = []
+        warning('No library found for ' + libname + ', skipping PAM tests')
+    endif
+endforeach
+
+envs.set('PPD_LD_PRELOAD', ' '.join(ppd_tests_ld_preload))
+
 foreach ut: unit_tests
-    ut_args = files('integration-test.py')
-    ut_args += ut
     test(ut,
          python3,
-         args: ut_args,
+         args: [
+            integration_tests,
+            ut,
+         ],
          env: envs,
         )
 endforeach
+
+if get_option('pylint')
+  integration_pylint_flags = ['-d', 'W0511', '-d', 'C0302'] + pylint_flags
+  test('pylint-integration-tests',
+       pylint,
+       args: integration_pylint_flags + integration_tests,
+       env: nomalloc,
+       )
+endif
diff --git a/tests/unittest_inspector.py b/tests/unittest_inspector.py
old mode 100755
new mode 100644
index 0d5d3a6..db595a4
--- a/tests/unittest_inspector.py
+++ b/tests/unittest_inspector.py
@@ -22,23 +22,25 @@ 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 ]
+            tests += [(obj, "{}.{}".format(name, t)) for t in cases]
     return tests
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     parser = argparse.ArgumentParser()
-    parser.add_argument('unittest_source', type=argparse.FileType('r'))
+    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)
+        os.path.basename(source_path), source_path
+    )
     module = importlib.util.module_from_spec(spec)
     spec.loader.exec_module(module)
 



More information about the Neon-commits mailing list