PhoenixCausesOof fabeee6ed7
Fix auto-cpufreq --monitor reporting the "CPU frequency scaling" section wrong. (#855)
It used to always read the config file's "battery" section and report that, even if connected to a charger. I used a hacky solution (simply check the state of the battery, i.e., charging or not charging) and read the correct part of the config file based on that.
2025-06-29 08:39:48 +02:00

346 lines
12 KiB
Python

from dataclasses import dataclass
import os
from pathlib import Path
import platform
from subprocess import getoutput
from typing import Tuple, List
import psutil
import distro
from pathlib import Path
from auto_cpufreq.config.config import config
from auto_cpufreq.core import get_power_supply_ignore_list
from auto_cpufreq.globals import (
AVAILABLE_GOVERNORS_SORTED,
CPU_TEMP_SENSOR_PRIORITY,
IS_INSTALLED_WITH_SNAP,
POWER_SUPPLY_DIR,
)
from typing import Optional
@dataclass
class CoreInfo:
id: int
usage: float
temperature: float
frequency: float
@dataclass
class BatteryInfo:
is_charging: bool | None
is_ac_plugged: bool | None
charging_start_threshold: int | None
charging_stop_threshold: int | None
battery_level: int | None
power_consumption: float | None
def __repr__(self) -> str:
if self.is_charging:
return "charging"
elif not self.is_ac_plugged:
return f"discharging {('(' + '{:.2f}'.format(self.power_consumption) + ' W)') if self.power_consumption != None else ''}"
return "Not Charging"
@dataclass
class SystemReport:
distro_name: str
distro_ver: str
arch: str
processor_model: str
total_core: int | None
kernel_version: str
current_gov: str | None
current_epp: str | None
current_epb: str | None
cpu_driver: str
cpu_fan_speed: int | None
cpu_usage: float
cpu_max_freq: float | None
cpu_min_freq: float | None
load: float
avg_load: Tuple[float, float, float] | None
cores_info: list[CoreInfo]
battery_info: BatteryInfo
is_turbo_on: Tuple[bool | None, bool | None]
class SystemInfo:
"""
Provides system information related to CPU, distribution, and performance metrics.
"""
def __init__(self):
self.distro_name: str = (
distro.name(pretty=True) if not IS_INSTALLED_WITH_SNAP else "UNKNOWN"
)
self.distro_version: str = (
distro.version() if not IS_INSTALLED_WITH_SNAP else "UNKNOWN"
)
self.architecture: str = platform.machine()
self.processor_model: str = (
getoutput("grep -E 'model name' /proc/cpuinfo -m 1").split(":")[-1].strip()
)
self.total_cores: int | None = psutil.cpu_count(logical=True)
self.cpu_driver: str = getoutput(
"cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver"
).strip()
self.kernel_version: str = platform.release()
@staticmethod
def cpu_min_freq() -> float | None:
freqs = psutil.cpu_freq(percpu=True)
return min((freq.min for freq in freqs), default=None)
@staticmethod
def cpu_max_freq() -> float | None:
freqs = psutil.cpu_freq(percpu=True)
return max((freq.max for freq in freqs), default=None)
@staticmethod
def get_cpu_info() -> List[CoreInfo]:
"""Returns detailed CPU information for each core."""
cpu_usage = psutil.cpu_percent(percpu=True)
cpu_freqs = psutil.cpu_freq(percpu=True)
try:
temps = psutil.sensors_temperatures()
temp_sensor = []
for sensor in CPU_TEMP_SENSOR_PRIORITY:
temp_sensor = temps.get(sensor, [])
if temp_sensor != []:
break
core_temps = [temp.current for temp in temp_sensor]
except AttributeError:
core_temps = []
avg_temp = sum(core_temps) / len(core_temps) if core_temps else 0.0
return [
CoreInfo(
id=i,
usage=cpu_usage[i],
temperature=core_temps[i] if i < len(core_temps) else avg_temp,
frequency=cpu_freqs[i].current,
)
for i in range(len(cpu_usage))
]
@staticmethod
def cpu_fan_speed() -> int | None:
fans = psutil.sensors_fans()
return next((fan[0].current for fan in fans.values() if fan), None)
@staticmethod
def current_gov() -> str | None:
try:
with open(
"/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor", "r"
) as f:
return f.read().strip()
except:
return None
@staticmethod
def current_epp(is_ac_plugged: bool) -> str | None:
epp_path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference"
if not Path(epp_path).exists():
return None
return config.get_config().get(
"charger" if is_ac_plugged else "battery", "energy_performance_preference", fallback="balance_power"
)
@staticmethod
def current_epb(is_ac_plugged: bool) -> str | None:
epb_path = "/sys/devices/system/cpu/intel_pstate"
if not Path(epb_path).exists():
return None
return config.get_config().get(
"charger" if is_ac_plugged else "battery", "energy_perf_bias", fallback="balance_power"
)
@staticmethod
def cpu_usage() -> float:
return psutil.cpu_percent(
interval=0.5
) # Reduced interval for better responsiveness
@staticmethod
def system_load() -> float:
return os.getloadavg()[0]
@staticmethod
def avg_load() -> Tuple[float, float, float]:
return os.getloadavg()
@staticmethod
def avg_temp() -> int:
temps: List[float] = [i.temperature for i in SystemInfo.get_cpu_info()]
return int(sum(temps) / len(temps))
@staticmethod
def turbo_on() -> Tuple[bool | None, bool | None]:
"""Get CPU turbo mode status.
Returns: Tuple[bool | None, bool | None]:
The first value indicates whether turbo mode is enabled, None if unknown
The second value indicates whether auto mode is enabled (amd_pstate only), None if unknown
"""
intel_pstate = Path("/sys/devices/system/cpu/intel_pstate/no_turbo")
cpu_freq = Path("/sys/devices/system/cpu/cpufreq/boost")
amd_pstate = Path("/sys/devices/system/cpu/amd_pstate/status")
if intel_pstate.exists():
control_file: Path = intel_pstate
inverse_logic = True
elif cpu_freq.exists():
control_file = cpu_freq
inverse_logic = False
elif amd_pstate.exists():
amd_status: str = amd_pstate.read_text().strip()
if amd_status == "active":
return None, True
return None, False
else:
return None, None
try:
current_value = int(control_file.read_text().strip())
return bool(current_value) ^ inverse_logic, False
except Exception as e:
return None, None
@staticmethod
def read_file(path: str) -> Optional[str]:
try:
with open(path, "r") as f:
return f.read().strip()
except (FileNotFoundError, OSError):
return None
@staticmethod
def get_battery_path() -> Optional[str]:
try:
for entry in os.listdir(POWER_SUPPLY_DIR):
path = os.path.join(POWER_SUPPLY_DIR, entry)
type_path = os.path.join(path, "type")
if os.path.isfile(type_path):
content = SystemInfo.read_file(type_path)
if content and content.lower() == "battery":
return path
except Exception:
return None
return None
@staticmethod
def battery_info() -> BatteryInfo:
battery_path = SystemInfo.get_battery_path()
# By default, AC is considered connected if no battery is detected
is_ac_plugged = True
is_charging = None
battery_level = None
power_consumption = None
charging_start_threshold = None
charging_stop_threshold = None
if not battery_path:
# No battery detected
return BatteryInfo(
is_charging=None,
is_ac_plugged=is_ac_plugged,
charging_start_threshold=None,
charging_stop_threshold=None,
battery_level=None,
power_consumption=None,
)
# Reading AC info (Hands)
for supply in os.listdir(POWER_SUPPLY_DIR):
supply_path = os.path.join(POWER_SUPPLY_DIR, supply)
supply_type = SystemInfo.read_file(os.path.join(supply_path, "type"))
if supply_type == "Mains":
online = SystemInfo.read_file(os.path.join(supply_path, "online"))
is_ac_plugged = online == "1"
# Reading battery information
battery_status = SystemInfo.read_file(os.path.join(battery_path, "status"))
battery_capacity = SystemInfo.read_file(os.path.join(battery_path, "capacity"))
energy_rate = (
SystemInfo.read_file(os.path.join(battery_path, "power_now"))
or SystemInfo.read_file(os.path.join(battery_path, "current_now"))
)
charge_start_threshold = SystemInfo.read_file(os.path.join(battery_path, "charge_start_threshold"))
charge_stop_threshold = SystemInfo.read_file(os.path.join(battery_path, "charge_stop_threshold"))
is_charging = battery_status.lower() == "charging" if battery_status else None
battery_level = int(battery_capacity) if battery_capacity and battery_capacity.isdigit() else None
power_consumption = float(energy_rate) / 1_000_000 if energy_rate \
and energy_rate.replace('.', '', 1).isdigit() else None
charging_start_threshold = int(charge_start_threshold) if charge_start_threshold \
and charge_start_threshold.isdigit() else None
charging_stop_threshold = int(charge_stop_threshold) if charge_stop_threshold \
and charge_stop_threshold.isdigit() else None
return BatteryInfo(
is_charging=is_charging,
is_ac_plugged=is_ac_plugged,
charging_start_threshold=charging_start_threshold,
charging_stop_threshold=charging_stop_threshold,
battery_level=battery_level,
power_consumption=power_consumption,
)
@staticmethod
def turbo_on_suggestion() -> bool:
usage = SystemInfo.cpu_usage()
if usage >= 20.0:
return True
elif usage <= 25 and SystemInfo.avg_temp() >= 70:
return False
return False
@staticmethod
def governor_suggestion() -> str:
if SystemInfo.battery_info().is_ac_plugged:
return AVAILABLE_GOVERNORS_SORTED[0]
return AVAILABLE_GOVERNORS_SORTED[-1]
def generate_system_report(self) -> SystemReport:
battery_info = self.battery_info()
return SystemReport(
distro_name=self.distro_name,
distro_ver=self.distro_version,
arch=self.architecture,
processor_model=self.processor_model,
total_core=self.total_cores,
cpu_driver=self.cpu_driver,
kernel_version=self.kernel_version,
current_gov=self.current_gov(),
current_epp=self.current_epp(battery_info.is_ac_plugged),
current_epb=self.current_epb(battery_info.is_ac_plugged),
cpu_fan_speed=self.cpu_fan_speed(),
cpu_usage=self.cpu_usage(),
cpu_max_freq=self.cpu_max_freq(),
cpu_min_freq=self.cpu_min_freq(),
load=self.system_load(),
avg_load=self.avg_load(),
cores_info=self.get_cpu_info(),
is_turbo_on=self.turbo_on(),
battery_info=battery_info,
)
system_info = SystemInfo()