feat(applications): add support for power-profiles-daemon/tuned as a power management daemon (#4015)

* fix(profiles): install power-profiles-daemon by default in the desktop
profile

* fix: only install power-profiles-daemon if a battery is detected

* chore: clean up has_battery method

* fix: make power management daemon a configurable application

* fix: make linter happy after merge

* fix: fix merge issues

* fix: give has_battery a return type to make linter happy

* chore: add locale msgids for power management related strings

* fix: changes requested in review

* fix: cache has_battery result

* fix: changes requested in review

* fix: just return none directly

* fix: add selected power management daemon to applications menu preview
This commit is contained in:
Mariya 2025-12-30 21:22:27 -05:00 committed by GitHub
parent 83c9bf06b2
commit 446d23c59d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 145 additions and 1 deletions

View File

@ -0,0 +1,35 @@
from typing import TYPE_CHECKING
from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
class PowerManagementApp:
@property
def ppd_packages(self) -> list[str]:
return [
'power-profiles-daemon',
]
@property
def tuned_packages(self) -> list[str]:
return [
'tuned',
'tuned-ppd',
]
def install(
self,
install_session: 'Installer',
power_management_config: PowerManagementConfiguration,
) -> None:
debug(f'Installing power management daemon: {power_management_config.power_management.value}')
match power_management_config.power_management:
case PowerManagement.POWER_PROFILES_DAEMON:
install_session.add_additional_packages(self.ppd_packages)
case PowerManagement.TUNED:
install_session.add_additional_packages(self.tuned_packages)

View File

@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
from archinstall.applications.audio import AudioApp from archinstall.applications.audio import AudioApp
from archinstall.applications.bluetooth import BluetoothApp from archinstall.applications.bluetooth import BluetoothApp
from archinstall.applications.power_management import PowerManagementApp
from archinstall.applications.print_service import PrintServiceApp from archinstall.applications.print_service import PrintServiceApp
from archinstall.lib.models import Audio from archinstall.lib.models import Audio
from archinstall.lib.models.application import ApplicationConfiguration from archinstall.lib.models.application import ApplicationConfiguration
@ -26,6 +27,12 @@ class ApplicationHandler:
users, users,
) )
if app_config.power_management_config:
PowerManagementApp().install(
install_session,
app_config.power_management_config,
)
if app_config.print_service_config and app_config.print_service_config.enabled: if app_config.print_service_config and app_config.print_service_config.enabled:
PrintServiceApp().install(install_session) PrintServiceApp().install(install_session)

View File

@ -1,7 +1,16 @@
from typing import override from typing import override
from archinstall.lib.hardware import SysInfo
from archinstall.lib.menu.abstract_menu import AbstractSubMenu from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration, PrintServiceConfiguration from archinstall.lib.models.application import (
ApplicationConfiguration,
Audio,
AudioConfiguration,
BluetoothConfiguration,
PowerManagement,
PowerManagementConfiguration,
PrintServiceConfiguration,
)
from archinstall.lib.translationhandler import tr from archinstall.lib.translationhandler import tr
from archinstall.tui.curses_menu import SelectMenu from archinstall.tui.curses_menu import SelectMenu
from archinstall.tui.menu_item import MenuItem, MenuItemGroup from archinstall.tui.menu_item import MenuItem, MenuItemGroup
@ -54,8 +63,21 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
preview_action=self._prev_print_service, preview_action=self._prev_print_service,
key='print_service_config', key='print_service_config',
), ),
MenuItem(
text=tr('Power management'),
action=select_power_management,
preview_action=self._prev_power_management,
enabled=SysInfo.has_battery(),
key='power_management_config',
),
] ]
def _prev_power_management(self, item: MenuItem) -> str | None:
if item.value is not None:
config: PowerManagementConfiguration = item.value
return f'{tr("Power management")}: {config.power_management.value}'
return None
def _prev_bluetooth(self, item: MenuItem) -> str | None: def _prev_bluetooth(self, item: MenuItem) -> str | None:
if item.value is not None: if item.value is not None:
bluetooth_config: BluetoothConfiguration = item.value bluetooth_config: BluetoothConfiguration = item.value
@ -81,6 +103,29 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
return None return None
def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None:
group = MenuItemGroup.from_enum(PowerManagement)
if preset:
group.set_focus_by_value(preset.power_management)
result = SelectMenu[PowerManagement](
group,
allow_skip=True,
alignment=Alignment.CENTER,
allow_reset=True,
frame=FrameProperties.min(tr('Power management')),
).run()
match result.type_:
case ResultType.Skip:
return preset
case ResultType.Selection:
return PowerManagementConfiguration(power_management=result.get_value())
case ResultType.Reset:
return None
def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None: def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
group = MenuItemGroup.yes_no() group = MenuItemGroup.yes_no()
group.focus_item = MenuItem.no() group.focus_item = MenuItem.no()

View File

@ -336,6 +336,11 @@ class GlobalMenu(AbstractMenu[None]):
output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled') output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
output += '\n' output += '\n'
if app_config.power_management_config:
power_management_config = app_config.power_management_config
output += f'{tr("Power management")}: {power_management_config.power_management.value}'
output += '\n'
return output return output
return None return None

View File

@ -143,6 +143,18 @@ class _SysInfo:
def __init__(self) -> None: def __init__(self) -> None:
pass pass
@cached_property
def has_battery(self) -> bool:
for type_path in Path('/sys/class/power_supply/').glob('*/type'):
try:
with open(type_path) as f:
if f.read().strip() == 'Battery':
return True
except OSError:
continue
return False
@cached_property @cached_property
def cpu_info(self) -> dict[str, str]: def cpu_info(self) -> dict[str, str]:
""" """
@ -210,6 +222,10 @@ _sys_info = _SysInfo()
class SysInfo: class SysInfo:
@staticmethod
def has_battery() -> bool:
return _sys_info.has_battery
@staticmethod @staticmethod
def has_wifi() -> bool: def has_wifi() -> bool:
ifaces = list(list_interfaces().values()) ifaces = list(list_interfaces().values())

View File

@ -3,6 +3,15 @@ from enum import StrEnum, auto
from typing import Any, NotRequired, TypedDict from typing import Any, NotRequired, TypedDict
class PowerManagement(StrEnum):
POWER_PROFILES_DAEMON = 'power-profiles-daemon'
TUNED = 'tuned'
class PowerManagementConfigSerialization(TypedDict):
power_management: str
class BluetoothConfigSerialization(TypedDict): class BluetoothConfigSerialization(TypedDict):
enabled: bool enabled: bool
@ -32,6 +41,7 @@ class ZramAlgorithm(StrEnum):
class ApplicationSerialization(TypedDict): class ApplicationSerialization(TypedDict):
bluetooth_config: NotRequired[BluetoothConfigSerialization] bluetooth_config: NotRequired[BluetoothConfigSerialization]
audio_config: NotRequired[AudioConfigSerialization] audio_config: NotRequired[AudioConfigSerialization]
power_management_config: NotRequired[PowerManagementConfigSerialization]
print_service_config: NotRequired[PrintServiceConfigSerialization] print_service_config: NotRequired[PrintServiceConfigSerialization]
@ -63,6 +73,22 @@ class BluetoothConfiguration:
return BluetoothConfiguration(arg['enabled']) return BluetoothConfiguration(arg['enabled'])
@dataclass
class PowerManagementConfiguration:
power_management: PowerManagement
def json(self) -> PowerManagementConfigSerialization:
return {
'power_management': self.power_management.value,
}
@staticmethod
def parse_arg(arg: dict[str, Any]) -> 'PowerManagementConfiguration':
return PowerManagementConfiguration(
PowerManagement(arg['power_management']),
)
@dataclass @dataclass
class PrintServiceConfiguration: class PrintServiceConfiguration:
enabled: bool enabled: bool
@ -94,6 +120,7 @@ class ZramConfiguration:
class ApplicationConfiguration: class ApplicationConfiguration:
bluetooth_config: BluetoothConfiguration | None = None bluetooth_config: BluetoothConfiguration | None = None
audio_config: AudioConfiguration | None = None audio_config: AudioConfiguration | None = None
power_management_config: PowerManagementConfiguration | None = None
print_service_config: PrintServiceConfiguration | None = None print_service_config: PrintServiceConfiguration | None = None
@staticmethod @staticmethod
@ -113,6 +140,9 @@ class ApplicationConfiguration:
if args and (audio_config := args.get('audio_config')) is not None: if args and (audio_config := args.get('audio_config')) is not None:
app_config.audio_config = AudioConfiguration.parse_arg(audio_config) app_config.audio_config = AudioConfiguration.parse_arg(audio_config)
if args and (power_management_config := args.get('power_management_config')) is not None:
app_config.power_management_config = PowerManagementConfiguration.parse_arg(power_management_config)
if args and (print_service_config := args.get('print_service_config')) is not None: if args and (print_service_config := args.get('print_service_config')) is not None:
app_config.print_service_config = PrintServiceConfiguration.parse_arg(print_service_config) app_config.print_service_config = PrintServiceConfiguration.parse_arg(print_service_config)
@ -127,6 +157,9 @@ class ApplicationConfiguration:
if self.audio_config: if self.audio_config:
config['audio_config'] = self.audio_config.json() config['audio_config'] = self.audio_config.json()
if self.power_management_config:
config['power_management_config'] = self.power_management_config.json()
if self.print_service_config: if self.print_service_config:
config['print_service_config'] = self.print_service_config.json() config['print_service_config'] = self.print_service_config.json()

View File

@ -1788,6 +1788,9 @@ msgstr ""
msgid "Would you like to configure the print service?" msgid "Would you like to configure the print service?"
msgstr "" msgstr ""
msgid "Power management"
msgstr ""
msgid "Authentication" msgid "Authentication"
msgstr "" msgstr ""