diff --git a/cpu_governor_auto_adjust.py b/cpu_governor_auto_adjust.py index b4ee263..3b05297 100755 --- a/cpu_governor_auto_adjust.py +++ b/cpu_governor_auto_adjust.py @@ -4,7 +4,7 @@ from pathlib import Path from logger import getLogger from mapping import trigger_mapping from config import Config -from governor import Governor +from governor import GovernorControl from schedule import TriggerScheduler @@ -15,7 +15,7 @@ async def main() -> None: if config.testmode: log.warning("starting in testmode, cpu adjustments have been disabled") - governor = Governor(config) + governor = GovernorControl(config) scheduler = TriggerScheduler(config) for triggertuple in config.triggertuples: _trigger = trigger_mapping[triggertuple.name](config) diff --git a/cpu_governor_auto_adjust.xml b/cpu_governor_auto_adjust.xml index b90245b..6bb4bae 100644 --- a/cpu_governor_auto_adjust.xml +++ b/cpu_governor_auto_adjust.xml @@ -31,4 +31,5 @@ + 5 diff --git a/governor.py b/governor.py index 16379d3..e8fa539 100644 --- a/governor.py +++ b/governor.py @@ -2,12 +2,26 @@ from app_class import AppClass from config import Config from cpufreq import cpuFreq, cpufreq # type: ignore from functools import cached_property -from mapping import governor_priority_mapping from exceptions import GovernorNotFound -from typing import Optional +from typing import Optional, NamedTuple + + +class Governor(NamedTuple): + name: str + priority: int + + +_governor_list: list[Governor] = [ + Governor("performance", 0), + Governor("schedutil", 1), + Governor("ondemand", 2), + Governor("conservative", 3), + Governor("userspace", 4), + Governor("powersave", 5) +] -class Governor(AppClass): +class GovernorControl(AppClass): def __init__(self, _config: Config) -> None: super().__init__(_config) self.log.info("current governor is: %s", self.current_governor) @@ -21,20 +35,27 @@ class Governor(AppClass): return None @property - def current_governor(self) -> Optional[str]: + def current_governor(self) -> Optional[Governor]: if self._cpufreq is None: return None _governor = set(self._cpufreq.get_governors().values()) - _governor_to_return = list(_governor)[0] + _governor_to_return = self._establish_governor(list(_governor)[0]) if len(_governor) == 0: raise GovernorNotFound("Unable to retrieve current governor") if len(_governor) > 1: self.log.error("multiple governors have been set, which is not expected.") # returning governor with highest priority based on performance for gov in _governor: - if governor_priority_mapping[gov] < governor_priority_mapping[_governor_to_return]: - _governor_to_return == gov - return _governor_to_return + t_gov = self._establish_governor(gov) + if t_gov.priority < _governor_to_return.priority: + _governor_to_return == t_gov + return _governor_to_return + + def _establish_governor(self, governor_name: str) -> Governor: + governor = next((g for g in _governor_list if g.name == governor_name), None) + if governor is None: + raise RuntimeError("could not establish governor") + return governor def set_governor(self, governor_name: str) -> None: self.log.debug( diff --git a/mapping.py b/mapping.py index 6d8eb39..532c932 100644 --- a/mapping.py +++ b/mapping.py @@ -7,14 +7,3 @@ _trigger_mapping = { 'roon': RoonTrigger } trigger_mapping = MappingProxyType(_trigger_mapping) - -_governor_priority_mapping = { - 'performance': 0, - 'schedutil': 1, - 'ondemand': 2, - 'conservative': 3, - 'userspace': 4, - 'performance': 5 -} - -governor_priority_mapping = MappingProxyType(_governor_priority_mapping) diff --git a/schedule.py b/schedule.py index 60334ae..1b338be 100644 --- a/schedule.py +++ b/schedule.py @@ -1,29 +1,44 @@ from app_class import AppClass from config import Config from trigger import Trigger +from governor import GovernorControl, Governor, _governor_list import asyncio from asyncio import Task +from functools import cached_property class TriggerScheduler(AppClass): def __init__(self, _config: Config) -> None: super().__init__(_config) self.tasks: list[Task] = [] + self.running_triggers: list[Trigger] = [] + + @cached_property + def governor_control(self) -> GovernorControl: + return GovernorControl(self._config) + + def establish_preferred_governor(self) -> str: + preferred_governor: Governor = [gov for gov in _governor_list if gov.name == 'powersave'].pop() + for trigger in self.running_triggers: + self.log.debug('trigger %s, state: %s, preferred governor: %s', trigger.name, trigger.trigger_state, trigger.governor.name) + if trigger.active: + if trigger.governor.priority < preferred_governor.priority: + preferred_governor = trigger.governor + self.log.debug('preferred governor: %s', preferred_governor.name) + return preferred_governor.name async def callback_trigger(self, _trigger: Trigger) -> None: """Run a callback_trigger with a specific name and check its status at a given interval.""" await _trigger.async_run() while True: - trigger_state = "active" if _trigger.active else "not active" - self.log.debug("Checking state of trigger %s, state: %s", _trigger.name, trigger_state) + self.log.debug("Checking state of trigger %s, state: %s", _trigger.name, _trigger.trigger_state) await asyncio.sleep(_trigger.config.interval_in_seconds) async def run_once_trigger(self, _trigger: Trigger) -> None: """Run a trigger with a specific name at a given interval.""" while True: _trigger.run() - trigger_state = "active" if _trigger.active else "not active" - self.log.debug("Checking state of trigger %s, state: %s", _trigger.name, trigger_state) + self.log.debug("Checking state of trigger %s, state: %s", _trigger.name, _trigger.trigger_state) await asyncio.sleep(_trigger.config.interval_in_seconds) def start_trigger(self, _trigger: Trigger) -> None: @@ -32,6 +47,7 @@ class TriggerScheduler(AppClass): task = asyncio.create_task(self.callback_trigger(_trigger)) else: task = asyncio.create_task(self.run_once_trigger(_trigger)) + self.running_triggers.append(_trigger) self.tasks.append(task) async def stop_triggers(self) -> None: @@ -44,6 +60,7 @@ class TriggerScheduler(AppClass): """Run the scheduler and keep it alive until stopped.""" try: while True: + self.establish_preferred_governor() await asyncio.sleep(1) # Keep the main function alive except asyncio.exceptions.CancelledError: self.log.info("Exiting, stopping all triggers") diff --git a/trigger.py b/trigger.py index 8240b91..0cf1e1d 100644 --- a/trigger.py +++ b/trigger.py @@ -1,12 +1,21 @@ from app_class import AppClass from config import Config, TriggerTuple from exceptions import MissingConfig, TriggerImportError +from governor import Governor, _governor_list +from functools import cached_property class Trigger(AppClass): def __init__(self, _config: Config) -> None: super().__init__(_config) self.active: bool = False + + def __hash__(self): + return super().__hash__(hash(self.name)) + + @property + def trigger_state(self) -> str: + return "active" if self.active else "not active" @property def name(self) -> str: @@ -24,6 +33,13 @@ class Trigger(AppClass): raise MissingConfig(f"Trigger {self.name} hasn't been properly configured") return _config + @cached_property + def governor(self) -> Governor: + _governor = next((gov for gov in _governor_list if self.config.governor == gov.name), None) + if _governor is None: + raise ValueError("unknown governor: %s", _governor) + return _governor + def run(self) -> None: # this is the main function of the trigger class and has to stay active by using a while True loop @@ -39,4 +55,3 @@ class Trigger(AppClass): raise TriggerImportError( "the Trigger class can't used directly, but must be inherited in a trigger specific class" ) - \ No newline at end of file