From 23eb67357172b52ea031d21562e38d6b5e62023d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20=C5=BD=C3=A1=C4=8Dik?= Date: Mon, 2 Dec 2024 09:14:14 +0100 Subject: [PATCH] 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. --- tuned/ppd/config.py | 12 +++++++ tuned/ppd/controller.py | 76 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/tuned/ppd/config.py b/tuned/ppd/config.py index b72b2311..7e148e10 100644 --- a/tuned/ppd/config.py +++ b/tuned/ppd/config.py @@ -11,6 +11,7 @@ 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 @@ def tuned_to_ppd(self): """ 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 @@ def load_from_file(self, config_file): 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) diff --git a/tuned/ppd/controller.py b/tuned/ppd/controller.py index b128c464..6e8a9391 100644 --- a/tuned/ppd/controller.py +++ b/tuned/ppd/controller.py @@ -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_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 @@ def process_IN_MODIFY(self, event): 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 @@ def __init__(self, bus, tuned_interface): 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 @@ def _setup_inotify(self): 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 @@ def check_performance_degraded(self): 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.