1
0
Fork 0

tuned-ppd: Enable changing profile via function keys

On some Thinkpad laptops, it is possible to change the ACPI
platform profile using function keys (e.g., Fn+L). This can
now trigger a complete profile change if the functionality
is enabled.

Resolves #710.
This commit is contained in:
Pavol Žáčik 2024-12-02 09:14:14 +01:00
parent 038dceadc0
commit 23eb673571
No known key found for this signature in database
GPG key ID: B99527D5E6487A92
2 changed files with 87 additions and 1 deletions

View file

@ -11,6 +11,7 @@ PROFILES_SECTION = "profiles"
BATTERY_SECTION = "battery"
DEFAULT_PROFILE_OPTION = "default"
BATTERY_DETECTION_OPTION = "battery_detection"
THINKPAD_FUNCTION_KEYS_OPTION = "thinkpad_function_keys"
class ProfileMap:
@ -73,6 +74,15 @@ class PPDConfig:
"""
return self._tuned_to_ppd
@property
def thinkpad_function_keys(self):
"""
Whether to react to changes of ACPI platform profile
done via function keys (e.g., Fn-L) on newer Thinkpad
machines. Experimental feature.
"""
return self._thinkpad_function_keys
def load_from_file(self, config_file):
"""
Loads the configuration from the provided file.
@ -130,3 +140,5 @@ class PPDConfig:
self._ppd_to_tuned = ProfileMap(profile_dict_ac, profile_dict_dc)
self._tuned_to_ppd = ProfileMap({v: k for k, v in profile_dict_ac.items()}, {v: k for k, v in profile_dict_dc.items()})
self._thinkpad_function_keys = cfg.getboolean(MAIN_SECTION, THINKPAD_FUNCTION_KEYS_OPTION, fallback=False)

View file

@ -1,12 +1,14 @@
from tuned import exports, logs
from tuned.utils.commands import commands
from tuned.consts import PPD_CONFIG_FILE, PPD_BASE_PROFILE_FILE, PPD_API_COMPATIBILITY
from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_POWER_SAVER
from tuned.ppd.config import PPDConfig, PPD_PERFORMANCE, PPD_BALANCED, PPD_POWER_SAVER
from enum import StrEnum
import pyinotify
import threading
import dbus
import os
import time
log = logs.get()
@ -19,6 +21,14 @@ UPOWER_DBUS_NAME = "org.freedesktop.UPower"
UPOWER_DBUS_PATH = "/org/freedesktop/UPower"
UPOWER_DBUS_INTERFACE = "org.freedesktop.UPower"
PLATFORM_PROFILE_PATH = "/sys/firmware/acpi/platform_profile"
PLATFORM_PROFILE_MAPPING = {
"low-power": PPD_POWER_SAVER,
"balanced": PPD_BALANCED,
"performance": PPD_PERFORMANCE
}
class PerformanceDegraded(StrEnum):
"""
Possible reasons for performance degradation.
@ -43,6 +53,51 @@ class PerformanceDegradedEventHandler(pyinotify.ProcessEvent):
self._controller.check_performance_degraded()
class PlatformProfileEventHandler(pyinotify.ProcessEvent):
"""
Event handler for switching PPD profiles based on the
ACPI platform profile
This handler should only invoke a PPD profile change if the
change of the file at PLATFORM_PROFILE_PATH comes from within
the kernel (e.g., when the user presses Fn-L on a Thinkpad laptop).
This is currently detected as the file being modified without
being opened before.
"""
CLOSE_MODIFY_BUFFER = 0.1
def __init__(self, controller):
super(PlatformProfileEventHandler, self).__init__()
self._controller = controller
self._file_open = False
self._last_close = 0
def process_IN_OPEN(self, event):
if event.pathname != PLATFORM_PROFILE_PATH:
return
self._file_open = True
self._last_close = 0
def process_IN_CLOSE_WRITE(self, event):
if event.pathname != PLATFORM_PROFILE_PATH:
return
self._file_open = False
self._last_close = time.time()
def process_IN_CLOSE_NOWRITE(self, event):
if event.pathname != PLATFORM_PROFILE_PATH:
return
self._file_open = False
def process_IN_MODIFY(self, event):
if event.pathname != PLATFORM_PROFILE_PATH or self._file_open or self._last_close + self.CLOSE_MODIFY_BUFFER > time.time():
# Do not invoke a profile change if a modify event comes:
# 1. when the file is open,
# 2. directly after the file is closed (the events may sometimes come in the wrong order).
return
self._controller.check_platform_profile()
class ProfileHold(object):
"""
Class holding information about a single profile hold,
@ -168,6 +223,7 @@ class Controller(exports.interfaces.ExportableInterface):
self._watch_manager = pyinotify.WatchManager()
self._notifier = pyinotify.ThreadedNotifier(self._watch_manager)
self._inotify_watches = {}
self._platform_profile_supported = os.path.isfile(PLATFORM_PROFILE_PATH)
self._no_turbo_supported = os.path.isfile(NO_TURBO_PATH)
self._lap_mode_supported = os.path.isfile(LAP_MODE_PATH)
self._tuned_interface.connect_to_signal("profile_changed", self._tuned_profile_changed)
@ -233,6 +289,10 @@ class Controller(exports.interfaces.ExportableInterface):
self._inotify_watches |= self._watch_manager.add_watch(path=os.path.dirname(LAP_MODE_PATH),
mask=pyinotify.IN_MODIFY,
proc_fun=PerformanceDegradedEventHandler(LAP_MODE_PATH, self))
if self._platform_profile_supported and self._config.thinkpad_function_keys:
self._inotify_watches |= self._watch_manager.add_watch(path=os.path.dirname(PLATFORM_PROFILE_PATH),
mask=pyinotify.IN_OPEN | pyinotify.IN_MODIFY | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CLOSE_NOWRITE,
proc_fun=PlatformProfileEventHandler(self))
def check_performance_degraded(self):
"""
@ -248,6 +308,20 @@ class Controller(exports.interfaces.ExportableInterface):
self._performance_degraded = performance_degraded
exports.property_changed("PerformanceDegraded", performance_degraded)
def check_platform_profile(self):
"""
Sets the active PPD profile based on the content of the ACPI platform profile.
"""
platform_profile = self._cmd.read_file(PLATFORM_PROFILE_PATH).strip()
if platform_profile not in PLATFORM_PROFILE_MAPPING:
return
log.debug("Platform profile changed: %s" % platform_profile)
new_profile = PLATFORM_PROFILE_MAPPING[platform_profile]
self._profile_holds.clear()
self.switch_profile(new_profile)
self._base_profile = new_profile
self._save_base_profile(new_profile)
def _load_base_profile(self):
"""
Loads and returns the saved PPD base profile.