mirror of
https://github.com/AdnanHodzic/auto-cpufreq.git
synced 2025-08-25 11:38:28 +02:00
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.
346 lines
12 KiB
Python
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()
|