diff --git a/library/sensors/sensors.py b/library/sensors/sensors.py index 0a9ada94..5f43209d 100644 --- a/library/sensors/sensors.py +++ b/library/sensors/sensors.py @@ -20,7 +20,7 @@ # To be overriden by child sensors classes from abc import ABC, abstractmethod -from typing import Tuple +from typing import List, Tuple class Cpu(ABC): @@ -77,6 +77,10 @@ def frequency() -> float: def is_available() -> bool: pass + @staticmethod + @abstractmethod + def get_gpu_names() -> List[str]: + pass class Memory(ABC): @staticmethod diff --git a/library/sensors/sensors_librehardwaremonitor.py b/library/sensors/sensors_librehardwaremonitor.py index f619a5b3..754bf6a7 100644 --- a/library/sensors/sensors_librehardwaremonitor.py +++ b/library/sensors/sensors_librehardwaremonitor.py @@ -25,7 +25,7 @@ import os import sys from statistics import mean -from typing import Tuple +from typing import List, Tuple import clr # Clr is from pythonnet package. Do not install clr package import psutil @@ -96,6 +96,17 @@ def get_hw_and_update(hwtype: Hardware.HardwareType, name: str = None) -> Hardwa return hardware return None +def get_all_gpus_and_update() -> List[Hardware.Hardware]: + hw_gpus = [] + for hardware in handle.Hardware: + if hardware.HardwareType in ( + Hardware.HardwareType.GpuNvidia, + Hardware.HardwareType.GpuAmd, + Hardware.HardwareType.GpuIntel + ): + hardware.Update() + hw_gpus.append(hardware) + return hw_gpus def get_gpu_name() -> str: # Determine which GPU to use, in case there are multiple : try to avoid using discrete GPU for stats @@ -160,14 +171,16 @@ def get_gpu_name() -> str: return gpu_to_use - def get_net_interface_and_update(if_name: str) -> Hardware.Hardware: for hardware in handle.Hardware: if hardware.HardwareType == Hardware.HardwareType.Network and hardware.Name == if_name: hardware.Update() return hardware - - logger.warning("Network interface '%s' not found. Check names in config.yaml." % if_name) + # Log only once per missing interface + if not hasattr(Net, '_logged_missing_lhm') or if_name not in Net._logged_missing_lhm: + logger.warning(f"LHM: Network interface '{if_name}' not found. Check names in config.yaml.") + if not hasattr(Net, '_logged_missing_lhm'): Net._logged_missing_lhm = set() + Net._logged_missing_lhm.add(if_name) return None @@ -175,12 +188,12 @@ class Cpu(sensors.Cpu): @staticmethod def percentage(interval: float) -> float: cpu = get_hw_and_update(Hardware.HardwareType.Cpu) + if not cpu: return math.nan for sensor in cpu.Sensors: if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith( "CPU Total") and sensor.Value is not None: return float(sensor.Value) - - logger.error("CPU load cannot be read") + logger.error("LHM: CPU load cannot be read") return math.nan @staticmethod @@ -207,11 +220,13 @@ def frequency() -> float: @staticmethod def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg (%): # Get this data from psutil because it is not available from LibreHardwareMonitor - return psutil.getloadavg() + try: return psutil.getloadavg() + except: return math.nan, math.nan, math.nan @staticmethod def temperature() -> float: cpu = get_hw_and_update(Hardware.HardwareType.Cpu) + if not cpu: return math.nan try: # By default, the average temperature of all CPU cores will be used for sensor in cpu.Sensors: @@ -233,23 +248,37 @@ def temperature() -> float: if sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith( "Core") and sensor.Value is not None: return float(sensor.Value) - except: - pass + except Exception as e: + logger.debug(f"LHM: Error reading CPU temperature: {e}") return math.nan @staticmethod def fan_percent(fan_name: str = None) -> float: mb = get_hw_and_update(Hardware.HardwareType.Motherboard) + controllers = [hw for hw in handle.Hardware if hw.HardwareType == Hardware.HardwareType.Controller] + try: - for sh in mb.SubHardware: - sh.Update() - for sensor in sh.Sensors: - if sensor.SensorType == Hardware.SensorType.Control and "#2" in str( - sensor.Name) and sensor.Value is not None: # Is Motherboard #2 Fan always the CPU Fan ? - return float(sensor.Value) - except: - pass + search_hardware = [mb] + controllers if mb else controllers + + for hw_container in search_hardware: + if not hw_container: continue + hw_container.Update() + + for sh in hw_container.SubHardware: + sh.Update() + + for sensor in sh.Sensors: + # Look for exact match + if fan_name and sensor.Identifier and fan_name == str(sensor.Identifier).replace('/control/', '/fan/') and sensor.SensorType == Hardware.SensorType.Control: + if sensor.Value is not None: + return float(sensor.Value) + # Try to find CPU fan + elif fan_name is None and sensor.SensorType == Hardware.SensorType.Control and ("cpu" in str(sensor.Name).lower() or "#2" in str(sensor.Name)): + if sensor.Value is not None: + return float(sensor.Value) + except Exception as e: + logger.debug(f"LHM: Error reading CPU fan speed: {e}") # No Fan Speed sensor for this CPU model return math.nan @@ -262,118 +291,143 @@ class Gpu(sensors.Gpu): # Latest FPS value is backed up in case next reading returns no value prev_fps = 0 - # Get GPU to use for sensors, and update it @classmethod - def get_gpu_to_use(cls): - gpu_to_use = get_hw_and_update(Hardware.HardwareType.GpuAmd, cls.gpu_name) - if gpu_to_use is None: - gpu_to_use = get_hw_and_update(Hardware.HardwareType.GpuNvidia, cls.gpu_name) - if gpu_to_use is None: - gpu_to_use = get_hw_and_update(Hardware.HardwareType.GpuIntel, cls.gpu_name) - - return gpu_to_use + def stats(cls) -> List[Tuple[float, float, float, float, float]]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) + # Returns stats for all GPUs found in the system + all_stats = [] + gpus_to_use = get_all_gpus_and_update() + + if not gpus_to_use: + # No supported GPUs found + return [] + + for gpu_hw in gpus_to_use: + load = math.nan + used_mem = math.nan + total_mem = math.nan + temp = math.nan + + try: + for sensor in gpu_hw.Sensors: + if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith( + "GPU Core") and sensor.Value is not None: + load = float(sensor.Value) + elif sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith("D3D 3D") and math.isnan( + load) and sensor.Value is not None: + # Only use D3D usage if global "GPU Core" sensor is not available + load = float(sensor.Value) + elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith( + "GPU Memory Used") and sensor.Value is not None: + used_mem = float(sensor.Value) + elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith( + "D3D") and str(sensor.Name).endswith("Memory Used") and math.isnan( + used_mem) and sensor.Value is not None: + # Only use D3D memory usage if global "GPU Memory Used" sensor is not available + used_mem = float(sensor.Value) + elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith( + "GPU Memory Total") and sensor.Value is not None: + total_mem = float(sensor.Value) + elif sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith( + "GPU Core") and sensor.Value is not None: + temp = float(sensor.Value) + + # Calculate memory percentage if possible + memory_percent = (used_mem / total_mem * 100.0) if not math.isnan(used_mem) and not math.isnan(total_mem) and total_mem > 0 else math.nan + + all_stats.append((load, memory_percent, used_mem, total_mem, temp)) + except Exception as e: + logger.debug(f"LHM: Error processing sensors for GPU {gpu_hw.Name}: {e}") + all_stats.append((math.nan, math.nan, math.nan, math.nan, math.nan)) + + return all_stats @classmethod - def stats(cls) -> Tuple[ - float, float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) - gpu_to_use = cls.get_gpu_to_use() - if gpu_to_use is None: - # GPU not supported - return math.nan, math.nan, math.nan, math.nan, math.nan - - load = math.nan - used_mem = math.nan - total_mem = math.nan - temp = math.nan - - for sensor in gpu_to_use.Sensors: - if sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith( - "GPU Core") and sensor.Value is not None: - load = float(sensor.Value) - elif sensor.SensorType == Hardware.SensorType.Load and str(sensor.Name).startswith("D3D 3D") and math.isnan( - load) and sensor.Value is not None: - # Only use D3D usage if global "GPU Core" sensor is not available, because it is less - # precise and does not cover the entire GPU: https://www.hwinfo.com/forum/threads/what-is-d3d-usage.759/ - load = float(sensor.Value) - elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith( - "GPU Memory Used") and sensor.Value is not None: - used_mem = float(sensor.Value) - elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith( - "D3D") and str(sensor.Name).endswith("Memory Used") and math.isnan( - used_mem) and sensor.Value is not None: - # Only use D3D memory usage if global "GPU Memory Used" sensor is not available, because it is less - # precise and does not cover the entire GPU: https://www.hwinfo.com/forum/threads/what-is-d3d-usage.759/ - used_mem = float(sensor.Value) - elif sensor.SensorType == Hardware.SensorType.SmallData and str(sensor.Name).startswith( - "GPU Memory Total") and sensor.Value is not None: - total_mem = float(sensor.Value) - elif sensor.SensorType == Hardware.SensorType.Temperature and str(sensor.Name).startswith( - "GPU Core") and sensor.Value is not None: - temp = float(sensor.Value) - - return load, (used_mem / total_mem * 100.0), used_mem, total_mem, temp + def get_gpu_names(cls) -> List[str]: + names = [] + gpus_to_use = get_all_gpus_and_update() + for gpu_hw in gpus_to_use: + names.append(gpu_hw.Name if gpu_hw.Name else "Unknown LHM GPU") + return names @classmethod - def fps(cls) -> int: - gpu_to_use = cls.get_gpu_to_use() - if gpu_to_use is None: - # GPU not supported - return -1 - - try: - for sensor in gpu_to_use.Sensors: - if sensor.SensorType == Hardware.SensorType.Factor and "FPS" in str( - sensor.Name) and sensor.Value is not None: - # If a reading returns a value <= 0, returns old value instead - if int(sensor.Value) > 0: - cls.prev_fps = int(sensor.Value) - return cls.prev_fps - except: - pass - - # No FPS sensor for this GPU model - return -1 + def fps(cls) -> List[int]: + # Returns FPS for all GPUs found in the system + all_fps = [] + gpus_to_use = get_all_gpus_and_update() + + if not gpus_to_use: + # No supported GPUs found + return [] + + for gpu_hw in gpus_to_use: + current_fps = -1 + try: + for sensor in gpu_hw.Sensors: + if sensor.SensorType == Hardware.SensorType.Factor and "FPS" in str(sensor.Name) and sensor.Value is not None: + # If a reading returns a valid value, use it + if int(sensor.Value) > 0: + current_fps = int(sensor.Value) + break + except Exception as e: + logger.debug(f"LHM: Error reading FPS for GPU {gpu_hw.Name}: {e}") + + all_fps.append(current_fps) + + return all_fps @classmethod - def fan_percent(cls) -> float: - gpu_to_use = cls.get_gpu_to_use() - if gpu_to_use is None: - # GPU not supported - return math.nan - - try: - for sensor in gpu_to_use.Sensors: - if sensor.SensorType == Hardware.SensorType.Control and sensor.Value is not None: - return float(sensor.Value) - except: - pass - - # No Fan Speed sensor for this GPU model - return math.nan + def fan_percent(cls) -> List[float]: + # Returns fan speed for all GPUs found in the system + all_fans = [] + gpus_to_use = get_all_gpus_and_update() + + if not gpus_to_use: + # No supported GPUs found + return [] + + for gpu_hw in gpus_to_use: + fan = math.nan + try: + for sensor in gpu_hw.Sensors: + if sensor.SensorType == Hardware.SensorType.Control and sensor.Value is not None: + fan = float(sensor.Value) + break + except Exception as e: + logger.debug(f"LHM: Error reading fan speed for GPU {gpu_hw.Name}: {e}") + + all_fans.append(fan) + + return all_fans @classmethod - def frequency(cls) -> float: - gpu_to_use = cls.get_gpu_to_use() - if gpu_to_use is None: - # GPU not supported - return math.nan - - try: - for sensor in gpu_to_use.Sensors: - if sensor.SensorType == Hardware.SensorType.Clock: - # Keep only real core clocks, ignore effective core clocks - if "Core" in str(sensor.Name) and "Effective" not in str(sensor.Name) and sensor.Value is not None: - return float(sensor.Value) - except: - pass - - # No Frequency sensor for this GPU model - return math.nan + def frequency(cls) -> List[float]: + # Returns core clock for all GPUs found in the system + all_freqs = [] + gpus_to_use = get_all_gpus_and_update() + + if not gpus_to_use: + # No supported GPUs found + return [] + + for gpu_hw in gpus_to_use: + freq = math.nan + try: + for sensor in gpu_hw.Sensors: + if sensor.SensorType == Hardware.SensorType.Clock: + # Keep only real core clocks, ignore effective core clocks + if "Core" in str(sensor.Name) and "Effective" not in str(sensor.Name) and sensor.Value is not None: + freq = float(sensor.Value) + break + except Exception as e: + logger.debug(f"LHM: Error reading frequency for GPU {gpu_hw.Name}: {e}") + + all_freqs.append(freq) + + return all_freqs @classmethod def is_available(cls) -> bool: - cls.gpu_name = get_gpu_name() - return bool(cls.gpu_name) + return bool(get_all_gpus_and_update()) class Memory(sensors.Memory): diff --git a/library/sensors/sensors_python.py b/library/sensors/sensors_python.py index 289e8800..726dd7df 100644 --- a/library/sensors/sensors_python.py +++ b/library/sensors/sensors_python.py @@ -24,7 +24,7 @@ import sys from collections import namedtuple from enum import IntEnum, auto -from typing import Tuple +from typing import Tuple, List # Added List # Nvidia GPU import GPUtil @@ -92,7 +92,15 @@ def sensors_fans(): min_rpm = int(bcat(base + '_min')) except: min_rpm = 0 # Approximated: min fan speed is 0 RPM - percent = int((current_rpm - min_rpm) / (max_rpm - min_rpm) * 100) + + # Avoid division by zero if max_rpm equals min_rpm + if max_rpm > min_rpm: + percent = int((current_rpm - min_rpm) / (max_rpm - min_rpm) * 100) + # Clamp percentage between 0 and 100 + percent = max(0, min(100, percent)) + else: + percent = 0 + except (IOError, OSError) as err: continue unit_name = cat(os.path.join(os.path.dirname(base), 'name')).strip() @@ -173,45 +181,105 @@ def fan_percent(fan_name: str = None) -> float: class Gpu(sensors.Gpu): @staticmethod - def stats() -> Tuple[ - float, float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) + def stats() -> List[Tuple[float, float, float, float, float]]: + # Returns list of: load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) per GPU global DETECTED_GPU if DETECTED_GPU == GpuType.AMD: return GpuAmd.stats() elif DETECTED_GPU == GpuType.NVIDIA: return GpuNvidia.stats() else: - return math.nan, math.nan, math.nan, math.nan, math.nan + return [] # Return empty list if no supported GPU @staticmethod - def fps() -> int: + def fps() -> List[int]: global DETECTED_GPU if DETECTED_GPU == GpuType.AMD: return GpuAmd.fps() elif DETECTED_GPU == GpuType.NVIDIA: return GpuNvidia.fps() else: - return -1 + return [] @staticmethod - def fan_percent() -> float: + def fan_percent() -> List[float]: + global DETECTED_GPU + num_gpus = 0 + expected_fan_dev_names = [] + + # Determine number of GPUs and expected device names for fan lookup + if DETECTED_GPU == GpuType.NVIDIA: + try: + num_gpus = len(GPUtil.getGPUs()) + expected_fan_dev_names = ['nouveau', 'nvidia'] + except: return [] + elif DETECTED_GPU == GpuType.AMD: + try: + if pyamdgpuinfo: + num_gpus = pyamdgpuinfo.detect_gpus() + expected_fan_dev_names = ['amdgpu', 'radeon'] + elif pyadl: + num_gpus = len(pyadl.ADLManager.getInstance().getDevices()) + expected_fan_dev_names = ['amdgpu', 'radeon'] + else: return [] + except: return [] + else: + return [] + + fan_percentages = [math.nan] * num_gpus + + try: + if platform.system() == "Linux": # Linux : sensors_fans + fans = sensors_fans() + fans_found_for_type = [] + # Find fans related to the GPU + for dev_name, entries in fans.items(): + if any(expected_name in dev_name.lower() for expected_name in expected_fan_dev_names): + for entry in entries: + if "gpu" in entry.label.lower() or "fan" in entry.label.lower(): # Broader check + fans_found_for_type.append(entry.percent) + + # Sequential mapping + for i in range(min(num_gpus, len(fans_found_for_type))): + fan_percentages[i] = fans_found_for_type[i] + except Exception as e: + logger.debug(f"sensors_fans check failed or not applicable: {e}") + pass + + if DETECTED_GPU == GpuType.AMD and pyadl and platform.system() == "Windows": # AMD gpu on Windows : pyadl + try: + devices = pyadl.ADLManager.getInstance().getDevices() + for i, device in enumerate(devices): + if i < num_gpus and math.isnan(fan_percentages[i]): # Only overwrite if the previous method resulted in NaN + try: + fan_percentages[i] = device.getCurrentFanSpeed(pyadl.ADL_DEVICE_FAN_SPEED_TYPE_PERCENTAGE) + except: + fan_percentages[i] = math.nan # Keep nan if pyadl fails + except Exception as e: + logger.debug(f"pyadl fan check failed: {e}") + pass + + return fan_percentages + + @staticmethod + def get_gpu_names() -> List[str]: global DETECTED_GPU if DETECTED_GPU == GpuType.AMD: - return GpuAmd.fan_percent() + return GpuAmd.get_gpu_names() elif DETECTED_GPU == GpuType.NVIDIA: - return GpuNvidia.fan_percent() + return GpuNvidia.get_gpu_names() else: - return math.nan + return [] @staticmethod - def frequency() -> float: + def frequency() -> List[float]: global DETECTED_GPU if DETECTED_GPU == GpuType.AMD: return GpuAmd.frequency() elif DETECTED_GPU == GpuType.NVIDIA: return GpuNvidia.frequency() else: - return math.nan + return [] @staticmethod def is_available() -> bool: @@ -236,65 +304,62 @@ def is_available() -> bool: class GpuNvidia(sensors.Gpu): @staticmethod - def stats() -> Tuple[ - float, float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) - # Unlike other sensors, Nvidia GPU with GPUtil pulls in all the stats at once - nvidia_gpus = GPUtil.getGPUs() - - try: - memory_used_all = [item.memoryUsed for item in nvidia_gpus] - memory_used_mb = sum(memory_used_all) / len(memory_used_all) - except: - memory_used_mb = math.nan - - try: - memory_total_all = [item.memoryTotal for item in nvidia_gpus] - memory_total_mb = sum(memory_total_all) / len(memory_total_all) - except: - memory_total_mb = math.nan - + def stats() -> List[Tuple[float, float, float, float, float]]: + # Returns list of: load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) per GPU + all_stats = [] try: - memory_percentage = (memory_used_mb / memory_total_mb) * 100 - except: - memory_percentage = math.nan + nvidia_gpus = GPUtil.getGPUs() + for gpu in nvidia_gpus: + load = gpu.load * 100 if gpu.load is not None else math.nan + memory_used_mb = gpu.memoryUsed if gpu.memoryUsed is not None else math.nan + memory_total_mb = gpu.memoryTotal if gpu.memoryTotal is not None else math.nan + + if not math.isnan(memory_used_mb) and not math.isnan(memory_total_mb) and memory_total_mb > 0: + memory_percentage = (memory_used_mb / memory_total_mb) * 100 + else: + memory_percentage = math.nan + + temperature = gpu.temperature if gpu.temperature is not None else math.nan + all_stats.append((load, memory_percentage, memory_used_mb, memory_total_mb, temperature)) + except Exception as e: + logger.error(f"Error getting Nvidia stats with GPUtil: {e}") + # Return list of nans if GPUtil fails entirely + try: num_gpus = len(GPUtil.getGPUs()) # Try to get count even on error + except: num_gpus = 1 # Assume 1 if count fails + return [(math.nan, math.nan, math.nan, math.nan, math.nan)] * num_gpus + return all_stats + @staticmethod + def get_gpu_names() -> List[str]: + names = [] try: - load_all = [item.load for item in nvidia_gpus] - load = (sum(load_all) / len(load_all)) * 100 - except: - load = math.nan - - try: - temperature_all = [item.temperature for item in nvidia_gpus] - temperature = sum(temperature_all) / len(temperature_all) - except: - temperature = math.nan - - return load, memory_percentage, memory_used_mb, memory_total_mb, temperature + nvidia_gpus = GPUtil.getGPUs() + for gpu in nvidia_gpus: + names.append(gpu.name if gpu.name else "NVIDIA GPU") + except Exception as e: + logger.error(f"Error getting Nvidia GPU names: {e}") + return names @staticmethod - def fps() -> int: - # Not supported by Python libraries - return -1 + def fps() -> List[int]: + # Not supported by the GPUtil library + try: num_gpus = len(GPUtil.getGPUs()) + except: num_gpus = 0 + return [-1] * num_gpus @staticmethod - def fan_percent() -> float: - try: - fans = sensors_fans() - if fans: - for name, entries in fans.items(): - for entry in entries: - if "gpu" in (entry.label.lower() or name.lower()): - return entry.percent - except: - pass - - return math.nan + def fan_percent() -> List[float]: + # Fan speed is handled by the main Gpu.fan_percent() method using OS interfaces, GPUtil doesn't provide fan speed directly. + try: num_gpus = len(GPUtil.getGPUs()) + except: num_gpus = 0 + return [math.nan] * num_gpus @staticmethod - def frequency() -> float: - # Not supported by Python libraries - return math.nan + def frequency() -> List[float]: + # Not supported by the GPUtil library + try: num_gpus = len(GPUtil.getGPUs()) + except: num_gpus = 0 + return [math.nan] * num_gpus @staticmethod def is_available() -> bool: @@ -306,96 +371,137 @@ def is_available() -> bool: class GpuAmd(sensors.Gpu): @staticmethod - def stats() -> Tuple[ - float, float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) + def stats() -> List[Tuple[float, float, float, float, float]]: + # Returns list of: load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) per GPU + all_stats = [] if pyamdgpuinfo: - # Unlike other sensors, AMD GPU with pyamdgpuinfo pulls in all the stats at once - pyamdgpuinfo.detect_gpus() - amd_gpu = pyamdgpuinfo.get_gpu(0) - try: - memory_used_bytes = amd_gpu.query_vram_usage() - memory_used = memory_used_bytes / 1024 / 1024 - except: - memory_used_bytes = math.nan - memory_used = math.nan + num_gpus = pyamdgpuinfo.detect_gpus() + for i in range(num_gpus): + load, memory_percentage, memory_used, memory_total, temperature = math.nan, math.nan, math.nan, math.nan, math.nan + try: + amd_gpu = pyamdgpuinfo.get_gpu(i) + try: memory_used_bytes = amd_gpu.query_vram_usage() + except: memory_used_bytes = math.nan + try: memory_total_bytes = amd_gpu.memory_info["vram_size"] + except: memory_total_bytes = math.nan + + if not math.isnan(memory_used_bytes) and not math.isnan(memory_total_bytes) and memory_total_bytes > 0: + memory_percentage = (memory_used_bytes / memory_total_bytes) * 100 + memory_used = memory_used_bytes / 1024 / 1024 + memory_total = memory_total_bytes / 1024 / 1024 + else: + memory_percentage, memory_used, memory_total = math.nan, math.nan, math.nan + + try: load = amd_gpu.query_load() * 100 + except: load = math.nan + try: temperature = amd_gpu.query_temperature() + except: temperature = math.nan + except Exception as gpu_err: + logger.debug(f"Error getting stats for AMD GPU {i} (pyamdgpuinfo): {gpu_err}") + all_stats.append((load, memory_percentage, memory_used, memory_total, temperature)) + except Exception as e: + logger.error(f"Error detecting AMD GPUs with pyamdgpuinfo: {e}") - try: - memory_total_bytes = amd_gpu.memory_info["vram_size"] - memory_total = memory_total_bytes / 1024 / 1024 - except: - memory_total_bytes = math.nan - memory_total = math.nan - - try: - memory_percentage = (memory_used_bytes / memory_total_bytes) * 100 - except: - memory_percentage = math.nan + elif pyadl: try: - load = amd_gpu.query_load() * 100 - except: - load = math.nan + devices = pyadl.ADLManager.getInstance().getDevices() + for amd_gpu in devices: + load, temperature = math.nan, math.nan + try: + try: load = amd_gpu.getCurrentUsage() + except: load = math.nan + try: temperature = amd_gpu.getCurrentTemperature() + except: temperature = math.nan + except Exception as gpu_err: + logger.debug(f"Error getting stats for AMD GPU (pyadl): {gpu_err}") + # pyadl doesn't easily provide memory details + all_stats.append((load, math.nan, math.nan, math.nan, temperature)) + except Exception as e: + logger.error(f"Error detecting AMD GPUs with pyadl: {e}") + + return all_stats + @staticmethod + def get_gpu_names() -> List[str]: + names = [] + if pyamdgpuinfo: try: - temperature = amd_gpu.query_temperature() - except: - temperature = math.nan - - return load, memory_percentage, memory_used, memory_total, temperature + num_gpus = pyamdgpuinfo.detect_gpus() + for i in range(num_gpus): + try: + name = pyamdgpuinfo.get_gpu(i).marketing_name + names.append(name if name else f"AMD GPU {i}") + except: + names.append(f"AMD GPU {i}") + except Exception as e: + logger.error(f"Error getting AMD GPU names (pyamdgpuinfo): {e}") elif pyadl: - amd_gpu = pyadl.ADLManager.getInstance().getDevices()[0] - - try: - load = amd_gpu.getCurrentUsage() - except: - load = math.nan - try: - temperature = amd_gpu.getCurrentTemperature() - except: - temperature = math.nan - - # GPU memory data not supported by pyadl - return load, math.nan, math.nan, math.nan, temperature + devices = pyadl.ADLManager.getInstance().getDevices() + for i, device in enumerate(devices): + try: + name = device.adapterName.decode('utf-8') + names.append(name if name else f"AMD GPU {i}") + except: + names.append(f"AMD GPU {i}") + except Exception as e: + logger.error(f"Error getting AMD GPU names (pyadl): {e}") + return names @staticmethod - def fps() -> int: + def fps() -> List[int]: # Not supported by Python libraries - return -1 + num_gpus = 0 + try: + if pyamdgpuinfo: num_gpus = pyamdgpuinfo.detect_gpus() + elif pyadl: num_gpus = len(pyadl.ADLManager.getInstance().getDevices()) + except: pass + return [-1] * num_gpus @staticmethod - def fan_percent() -> float: + def fan_percent() -> List[float]: + # Fan speed is handled by the main Gpu.fan_percent method using OS interfaces or pyadl + num_gpus = 0 try: - # Try with psutil fans - fans = sensors_fans() - if fans: - for name, entries in fans.items(): - for entry in entries: - if "gpu" in (entry.label.lower() or name.lower()): - return entry.percent + if pyamdgpuinfo: num_gpus = pyamdgpuinfo.detect_gpus() + elif pyadl: num_gpus = len(pyadl.ADLManager.getInstance().getDevices()) + except: pass + return [math.nan] * num_gpus # Return list of nans, main method handles it - # Try with pyadl if psutil did not find GPU fan - if pyadl: - return pyadl.ADLManager.getInstance().getDevices()[0].getCurrentFanSpeed( - pyadl.ADL_DEVICE_FAN_SPEED_TYPE_PERCENTAGE) - except: - pass + @staticmethod + def frequency() -> List[float]: # Returns list of MHz + frequencies = [] + if pyamdgpuinfo: + try: + num_gpus = pyamdgpuinfo.detect_gpus() + frequencies = [math.nan] * num_gpus + for i in range(num_gpus): + try: frequencies[i] = pyamdgpuinfo.get_gpu(i).query_sclk() + except: pass # Keep nan on error + + except Exception as e: + logger.error(f"Error detecting AMD GPU frequency with pyamdgpuinfo: {e}") + try: num_gpus = pyamdgpuinfo.detect_gpus() # Try to determine num_gpus anyway + except: num_gpus = 1 # Assume 1 if count fails + return [math.nan] * num_gpus - return math.nan + elif pyadl: + try: + devices = pyadl.ADLManager.getInstance().getDevices() + frequencies = [math.nan] * len(devices) + for i, device in enumerate(devices): + try: frequencies[i] = device.getCurrentEngineClock() # Returns MHz + except: pass # Keep nan on error + + except Exception as e: + logger.error(f"Error detecting AMD GPU frequency with pyadl: {e}") + try: num_gpus = len(pyadl.ADLManager.getInstance().getDevices()) # Try to determine num_gpus anyway + except: num_gpus = 1 # Assume 1 if count fails + return [math.nan] * num_gpus + return frequencies - @staticmethod - def frequency() -> float: - try: - if pyamdgpuinfo: - pyamdgpuinfo.detect_gpus() - return pyamdgpuinfo.get_gpu(0).query_sclk() / 1000000 - elif pyadl: - return pyadl.ADLManager.getInstance().getDevices()[0].getCurrentEngineClock() - else: - return math.nan - except: - return math.nan @staticmethod def is_available() -> bool: @@ -484,18 +590,41 @@ def stats(if_name, interval) -> Tuple[ if if_name != "": if if_name in pnic_after: try: - upload_rate = (pnic_after[if_name].bytes_sent - PNIC_BEFORE[if_name].bytes_sent) / interval + # Ensure interval is not zero and we have previous data + if interval > 0 and if_name in PNIC_BEFORE: + upload_rate = (pnic_after[if_name].bytes_sent - PNIC_BEFORE[if_name].bytes_sent) / interval + download_rate = (pnic_after[if_name].bytes_recv - PNIC_BEFORE[if_name].bytes_recv) / interval + # Prevent negative rates if counters reset + upload_rate = max(0, upload_rate) + download_rate = max(0, download_rate) + else: + upload_rate = 0 + download_rate = 0 + uploaded = pnic_after[if_name].bytes_sent - download_rate = (pnic_after[if_name].bytes_recv - PNIC_BEFORE[if_name].bytes_recv) / interval downloaded = pnic_after[if_name].bytes_recv - except: - # Interface might not be in PNIC_BEFORE for now - pass + + except KeyError: # Handles the case where if_name is not in PNIC_BEFORE yet + upload_rate = 0 + download_rate = 0 + uploaded = pnic_after[if_name].bytes_sent + downloaded = pnic_after[if_name].bytes_recv + except Exception as e: + logger.debug(f"Error calculating net stats for {if_name}: {e}") + upload_rate, uploaded, download_rate, downloaded = 0, 0, 0, 0 PNIC_BEFORE.update({if_name: pnic_after[if_name]}) else: - logger.warning("Network interface '%s' not found. Check names in config.yaml." % if_name) - - return upload_rate, uploaded, download_rate, downloaded - except: - return -1, -1, -1, -1 + # Log only once per missing interface to avoid spamming + if not hasattr(Net, '_logged_missing') or if_name not in Net._logged_missing: + logger.warning(f"Network interface '{if_name}' not found in psutil.net_io_counters(). Check names in config.yaml.") + if not hasattr(Net, '_logged_missing'): Net._logged_missing = set() + Net._logged_missing.add(if_name) + upload_rate, uploaded, download_rate, downloaded = 0, 0, 0, 0 + + + return int(upload_rate), int(uploaded), int(download_rate), int(downloaded) + + except Exception as e: + logger.error(f"General error fetching network stats: {e}") + return -1, -1, -1, -1 \ No newline at end of file diff --git a/library/sensors/sensors_stub_random.py b/library/sensors/sensors_stub_random.py index 07e37748..64fee830 100644 --- a/library/sensors/sensors_stub_random.py +++ b/library/sensors/sensors_stub_random.py @@ -20,7 +20,7 @@ # For all platforms (Linux, Windows, macOS) import random -from typing import Tuple +from typing import Tuple, List import library.sensors.sensors as sensors @@ -49,22 +49,26 @@ def fan_percent(fan_name: str = None) -> float: class Gpu(sensors.Gpu): @staticmethod - def stats() -> Tuple[ - float, float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) - return random.uniform(0, 100), random.uniform(0, 100), random.uniform(300, 16000), 16000.0, random.uniform(30, - 90) + def stats() -> List[Tuple[float, float, float, float, float]]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) + gpu0 = (random.uniform(0, 100), random.uniform(0, 100), random.uniform(300, 16000), 16000.0, random.uniform(30, 90)) + gpu1 = (random.uniform(0, 100), random.uniform(0, 100), random.uniform(300, 16000), 16000.0, random.uniform(30, 90)) + return [gpu0, gpu1] @staticmethod - def fps() -> int: - return random.randint(20, 120) + def get_gpu_names() -> List[str]: + return ["Dummy GPU 0 (Rand)", "Dummy GPU 1 (Rand)"] @staticmethod - def fan_percent() -> float: - return random.uniform(0, 100) + def fps() -> List[int]: + return [random.randint(20, 120), random.randint(20, 120)] @staticmethod - def frequency() -> float: - return random.uniform(800, 3400) + def fan_percent() -> List[float]: + return [random.uniform(0, 100), random.uniform(0, 100)] + + @staticmethod + def frequency() -> List[float]: + return [random.uniform(800, 3400), random.uniform(800, 3400)] @staticmethod def is_available() -> bool: diff --git a/library/sensors/sensors_stub_static.py b/library/sensors/sensors_stub_static.py index 5aa16168..b3651922 100644 --- a/library/sensors/sensors_stub_static.py +++ b/library/sensors/sensors_stub_static.py @@ -20,7 +20,7 @@ # Useful for theme editor # For all platforms (Linux, Windows, macOS) -from typing import Tuple +from typing import Tuple, List import library.sensors.sensors as sensors @@ -32,11 +32,28 @@ CPU_FREQ_MHZ = 2400.0 DISK_TOTAL_SIZE_GB = 1000 MEMORY_TOTAL_SIZE_GB = 64 -GPU_MEM_TOTAL_SIZE_GB = 32 NETWORK_SPEED_BYTES = 1061000000 +GPU_MEM_TOTAL_SIZE_GB = 32 GPU_FPS = 120 GPU_FREQ_MHZ = 1500.0 +# Define first GPU values +GPU1_PERCENTAGE = PERCENTAGE_SENSOR_VALUE +GPU1_MEM_PERCENTAGE = PERCENTAGE_SENSOR_VALUE +GPU1_MEM_TOTAL_SIZE_GB = GPU_MEM_TOTAL_SIZE_GB +GPU1_TEMPERATURE = TEMPERATURE_SENSOR_VALUE +GPU1_FPS = GPU_FPS +GPU1_FREQ_MHZ = GPU_FREQ_MHZ +GPU1_FAN_PERCENT = PERCENTAGE_SENSOR_VALUE + +# Define second GPU values +GPU2_PERCENTAGE = PERCENTAGE_SENSOR_VALUE +GPU2_MEM_PERCENTAGE = PERCENTAGE_SENSOR_VALUE +GPU2_MEM_TOTAL_SIZE_GB = GPU_MEM_TOTAL_SIZE_GB +GPU2_TEMPERATURE = TEMPERATURE_SENSOR_VALUE +GPU2_FPS = GPU_FPS +GPU2_FREQ_MHZ = GPU_FREQ_MHZ +GPU2_FAN_PERCENT = PERCENTAGE_SENSOR_VALUE class Cpu(sensors.Cpu): @staticmethod @@ -62,25 +79,36 @@ def fan_percent(fan_name: str = None) -> float: class Gpu(sensors.Gpu): @staticmethod - def stats() -> Tuple[ - float, float, float, float, float]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) - return (PERCENTAGE_SENSOR_VALUE, - PERCENTAGE_SENSOR_VALUE, - GPU_MEM_TOTAL_SIZE_GB / 100 * PERCENTAGE_SENSOR_VALUE * 1024, - GPU_MEM_TOTAL_SIZE_GB * 1024, - TEMPERATURE_SENSOR_VALUE) + def stats() -> List[Tuple[float, float, float, float, float]]: # load (%) / used mem (%) / used mem (Mb) / total mem (Mb) / temp (°C) + gpu1 = (GPU1_PERCENTAGE, + GPU1_MEM_PERCENTAGE, + GPU1_MEM_TOTAL_SIZE_GB / 100 * GPU1_MEM_PERCENTAGE * 1024, + GPU1_MEM_TOTAL_SIZE_GB * 1024, + GPU1_TEMPERATURE) + + gpu2 = (GPU2_PERCENTAGE, + GPU2_MEM_PERCENTAGE, + GPU2_MEM_TOTAL_SIZE_GB / 100 * GPU2_MEM_PERCENTAGE * 1024, + GPU2_MEM_TOTAL_SIZE_GB * 1024, + GPU2_TEMPERATURE) + + return [gpu1, gpu2] @staticmethod - def fps() -> int: - return GPU_FPS + def get_gpu_names() -> List[str]: + return ["Dummy GPU 0 (Static)", "Dummy GPU 1 (Static)"] @staticmethod - def fan_percent() -> float: - return PERCENTAGE_SENSOR_VALUE + def fps() -> List[int]: + return [GPU1_FPS, GPU2_FPS] @staticmethod - def frequency() -> float: - return GPU_FREQ_MHZ + def fan_percent() -> List[float]: + return [GPU1_FAN_PERCENT, GPU2_FAN_PERCENT] + + @staticmethod + def frequency() -> List[float]: + return [GPU1_FREQ_MHZ, GPU2_FREQ_MHZ] @staticmethod def is_available() -> bool: @@ -123,4 +151,4 @@ class Net(sensors.Net): @staticmethod def stats(if_name, interval) -> Tuple[ int, int, int, int]: # up rate (B/s), uploaded (B), dl rate (B/s), downloaded (B) - return NETWORK_SPEED_BYTES, NETWORK_SPEED_BYTES, NETWORK_SPEED_BYTES, NETWORK_SPEED_BYTES + return NETWORK_SPEED_BYTES, NETWORK_SPEED_BYTES, NETWORK_SPEED_BYTES, NETWORK_SPEED_BYTES \ No newline at end of file diff --git a/library/stats.py b/library/stats.py index fa1e20ee..8c824fbf 100644 --- a/library/stats.py +++ b/library/stats.py @@ -26,7 +26,7 @@ import os import platform import sys -from typing import List +from typing import List, Tuple, Callable # Added List, Tuple, Callable import babel.dates import requests @@ -119,18 +119,20 @@ def display_themed_value(theme_data, value, min_size=0, unit=''): def display_themed_percent_value(theme_data, value): + display_val = 0 if math.isnan(value) else int(value) display_themed_value( theme_data=theme_data, - value=int(value), + value=display_val, min_size=3, unit="%" ) def display_themed_temperature_value(theme_data, value): + display_val = 0 if math.isnan(value) else int(value) display_themed_value( theme_data=theme_data, - value=int(value), + value=display_val, min_size=3, unit="°C" ) @@ -140,12 +142,13 @@ def display_themed_progress_bar(theme_data, value): if not theme_data.get("SHOW", False): return + display_val = 0 if math.isnan(value) else int(value) display.lcd.DisplayProgressBar( x=theme_data.get("X", 0), y=theme_data.get("Y", 0), width=theme_data.get("WIDTH", 0), height=theme_data.get("HEIGHT", 0), - value=int(value), + value=display_val, # Use the checked value min_value=theme_data.get("MIN_VALUE", 0), max_value=theme_data.get("MAX_VALUE", 100), bar_color=theme_data.get("BAR_COLOR", (0, 0, 0)), @@ -159,11 +162,12 @@ def display_themed_radial_bar(theme_data, value, min_size=0, unit='', custom_tex if not theme_data.get("SHOW", False): return + display_val = 0 if math.isnan(value) else value if theme_data.get("SHOW_TEXT", False): if custom_text: text = custom_text else: - text = f"{{:>{min_size}}}".format(value) + text = f"{{:>{min_size}}}".format(int(display_val) if not isinstance(display_val, str) else display_val) # Ensure int for formatting if not already string if theme_data.get("SHOW_UNIT", True) and unit: text += str(unit) else: @@ -181,7 +185,7 @@ def display_themed_radial_bar(theme_data, value, min_size=0, unit='', custom_tex angle_steps=theme_data.get("ANGLE_STEPS", 1), angle_sep=theme_data.get("ANGLE_SEP", 0), clockwise=theme_data.get("CLOCKWISE", False), - value=value, + value=display_val, bar_color=theme_data.get("BAR_COLOR", (0, 0, 0)), text=text, font=config.FONTS_DIR + theme_data.get("FONT", "roboto-mono/RobotoMono-Regular.ttf"), @@ -198,9 +202,10 @@ def display_themed_radial_bar(theme_data, value, min_size=0, unit='', custom_tex def display_themed_percent_radial_bar(theme_data, value): + display_val = 0 if math.isnan(value) else int(value) display_themed_radial_bar( theme_data=theme_data, - value=int(value), + value=display_val, unit="%", min_size=3 ) @@ -372,6 +377,16 @@ def fan_speed(cls): class Gpu: + loads = [] + memory_percentages = [] + memory_used_mbs = [] + total_memory_mbs = [] + temperatures = [] + fps_values = [] + fan_percents = [] + frequencies_ghz = [] + gpu_names = [] # Added GPU names storage array + last_values_gpu_percentage = [] last_values_gpu_mem_percentage = [] last_values_gpu_temperature = [] @@ -381,37 +396,155 @@ class Gpu: @classmethod def stats(cls): - load, memory_percentage, memory_used_mb, total_memory_mb, temperature = sensors.Gpu.stats() - fps = sensors.Gpu.fps() - fan_percent = sensors.Gpu.fan_percent() - freq_ghz = sensors.Gpu.frequency() / 1000 + """Main entry point for GPU statistics collection and display""" + # 1. Fetch data and prepare storage + num_gpus = cls._fetch_gpu_data() + + # 2. Process each GPU's data and update histories + for i in range(num_gpus): + cls._update_history(i) + + # 3. Render multi-GPU data (new structure with GPU0, GPU1, etc.) + cls._render_multi_gpu(num_gpus) + + # 4. Handle legacy/backward compatibility (first GPU) + if num_gpus > 0: + cls._render_legacy_gpu() + + @classmethod + def _fetch_gpu_data(cls): + """Fetch GPU data and resize storage if needed""" + all_stats_tuples = sensors.Gpu.stats() + all_fps = sensors.Gpu.fps() + all_fan_percent = sensors.Gpu.fan_percent() + all_frequency_mhz = sensors.Gpu.frequency() + all_names = sensors.Gpu.get_gpu_names() + + num_gpus = len(all_stats_tuples) + + if len(all_names) < num_gpus: + all_names.extend([f"GPU {i}" for i in range(len(all_names), num_gpus)]) + elif len(all_names) > num_gpus: + all_names = all_names[:num_gpus] + + if len(cls.loads) != num_gpus: + cls._resize_storage(num_gpus) + + for i in range(num_gpus): + if i < len(all_stats_tuples): + cls.loads[i], cls.memory_percentages[i], cls.memory_used_mbs[i], cls.total_memory_mbs[i], cls.temperatures[i] = all_stats_tuples[i] + else: + cls.loads[i], cls.memory_percentages[i], cls.memory_used_mbs[i], cls.total_memory_mbs[i], cls.temperatures[i] = [math.nan]*5 + + cls.fps_values[i] = all_fps[i] if i < len(all_fps) else -1 + cls.fan_percents[i] = all_fan_percent[i] if i < len(all_fan_percent) else math.nan + freq_mhz = all_frequency_mhz[i] if i < len(all_frequency_mhz) else math.nan + cls.frequencies_ghz[i] = freq_mhz / 1000.0 if freq_mhz is not None and not math.isnan(freq_mhz) else math.nan + cls.gpu_names[i] = all_names[i] + + return num_gpus + + @classmethod + def _render_multi_gpu(cls, num_gpus): + """Render metrics for multiple GPUs using the GPU0, GPU1, etc. structure""" + theme_gpu_main_data = config.THEME_DATA['STATS']['GPU'] + for i in range(num_gpus): + gpu_key = f"GPU{i}" + if gpu_key in theme_gpu_main_data: + gpu_theme_section = theme_gpu_main_data[gpu_key] + + # Render GPU name if configured in theme + if 'NAME' in gpu_theme_section and 'TEXT' in gpu_theme_section['NAME']: + display_themed_value(gpu_theme_section['NAME']['TEXT'], cls.gpu_names[i]) + + # Render stats using helper methods + cls._render_gpu_stat(gpu_theme_section, 'PERCENTAGE', cls.loads[i], + cls.last_values_gpu_percentage[i], + display_themed_percent_value, display_themed_percent_radial_bar) + + cls._render_gpu_stat(gpu_theme_section, 'MEMORY_PERCENT', cls.memory_percentages[i], + cls.last_values_gpu_mem_percentage[i], + display_themed_percent_value, display_themed_percent_radial_bar) + + cls._render_gpu_stat(gpu_theme_section, 'TEMPERATURE', cls.temperatures[i], + cls.last_values_gpu_temperature[i], + display_themed_temperature_value, display_themed_temperature_radial_bar) + + cls._render_gpu_stat(gpu_theme_section, 'FAN_SPEED', cls.fan_percents[i], + cls.last_values_gpu_fan_speed[i], + display_themed_percent_value, display_themed_percent_radial_bar) + + cls._render_gpu_stat_custom_format(gpu_theme_section, 'MEMORY_USED', + cls.memory_used_mbs[i], unit=" M", min_size=5) + + cls._render_gpu_stat_custom_format(gpu_theme_section, 'MEMORY_TOTAL', + cls.total_memory_mbs[i], unit=" M", min_size=5) + + cls._render_gpu_stat_custom_format(gpu_theme_section, 'FPS', cls.fps_values[i], + unit=" FPS", min_size=4, + history_list=cls.last_values_gpu_fps[i]) + + cls._render_gpu_stat_custom_format(gpu_theme_section, 'FREQUENCY', cls.frequencies_ghz[i], + unit=" GHz", min_size=4, format_str='{:.2f}', + history_list=cls.last_values_gpu_frequency[i]) + + @classmethod + def _render_legacy_gpu(cls): + """Render metrics for the first GPU in the legacy format for backward compatibility""" theme_gpu_data = config.THEME_DATA['STATS']['GPU'] + + # Extract data for the first GPU + load = cls.loads[0] + memory_percentage = cls.memory_percentages[0] + memory_used_mb = cls.memory_used_mbs[0] + total_memory_mb = cls.total_memory_mbs[0] + temperature = cls.temperatures[0] + fps = cls.fps_values[0] + fan_percent = cls.fan_percents[0] + freq_ghz = cls.frequencies_ghz[0] + + # Legacy memory section + cls._render_legacy_memory(theme_gpu_data, memory_percentage, memory_used_mb) + + # GPU load percentage + cls._render_legacy_percentage(theme_gpu_data, load) + + # GPU memory percentage + cls._render_legacy_memory_percent(theme_gpu_data, memory_percentage) + + # GPU memory used + cls._render_legacy_memory_used(theme_gpu_data, memory_used_mb) + + # GPU total memory + cls._render_legacy_memory_total(theme_gpu_data, total_memory_mb) + + # GPU temperature + cls._render_legacy_temperature(theme_gpu_data, temperature) + + # GPU FPS + cls._render_legacy_fps(theme_gpu_data, fps) + + # GPU fan speed + cls._render_legacy_fan_speed(theme_gpu_data, fan_percent) + + # GPU frequency + cls._render_legacy_frequency(theme_gpu_data, freq_ghz) - save_last_value(load, cls.last_values_gpu_percentage, - theme_gpu_data['PERCENTAGE']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) - save_last_value(memory_percentage, cls.last_values_gpu_mem_percentage, - theme_gpu_data['MEMORY_PERCENT']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) - save_last_value(temperature, cls.last_values_gpu_temperature, - theme_gpu_data['TEMPERATURE']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) - save_last_value(fps, cls.last_values_gpu_fps, - theme_gpu_data['FPS']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) - save_last_value(fan_percent, cls.last_values_gpu_fan_speed, - theme_gpu_data['FAN_SPEED']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) - save_last_value(freq_ghz, cls.last_values_gpu_frequency, - theme_gpu_data['FREQUENCY']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) - - ################################ for backward compatibility only + @classmethod + def _render_legacy_memory(cls, theme_gpu_data, memory_percentage, memory_used_mb): + """Render legacy memory section""" gpu_mem_graph_data = theme_gpu_data['MEMORY']['GRAPH'] gpu_mem_radial_data = theme_gpu_data['MEMORY']['RADIAL'] + gpu_mem_text_data = theme_gpu_data['MEMORY']['TEXT'] + if math.isnan(memory_percentage): memory_percentage = 0 if gpu_mem_graph_data['SHOW'] or gpu_mem_radial_data['SHOW']: logger.warning("Your GPU memory relative usage (%) is not supported yet") gpu_mem_graph_data['SHOW'] = False gpu_mem_radial_data['SHOW'] = False - - gpu_mem_text_data = theme_gpu_data['MEMORY']['TEXT'] + if math.isnan(memory_used_mb): memory_used_mb = 0 if gpu_mem_text_data['SHOW']: @@ -426,9 +559,10 @@ def stats(cls): min_size=5, unit=" M" ) - ################################ end of backward compatibility only - # GPU usage (%) + @classmethod + def _render_legacy_percentage(cls, theme_gpu_data, load): + """Render legacy GPU load percentage""" gpu_percent_graph_data = theme_gpu_data['PERCENTAGE']['GRAPH'] gpu_percent_radial_data = theme_gpu_data['PERCENTAGE']['RADIAL'] gpu_percent_text_data = theme_gpu_data['PERCENTAGE']['TEXT'] @@ -447,9 +581,11 @@ def stats(cls): display_themed_progress_bar(gpu_percent_graph_data, load) display_themed_percent_radial_bar(gpu_percent_radial_data, load) display_themed_percent_value(gpu_percent_text_data, load) - display_themed_line_graph(gpu_percent_line_graph_data, cls.last_values_gpu_percentage) + display_themed_line_graph(gpu_percent_line_graph_data, cls.last_values_gpu_percentage[0]) - # GPU mem. usage (%) + @classmethod + def _render_legacy_memory_percent(cls, theme_gpu_data, memory_percentage): + """Render legacy GPU memory percentage""" gpu_mem_percent_graph_data = theme_gpu_data['MEMORY_PERCENT']['GRAPH'] gpu_mem_percent_radial_data = theme_gpu_data['MEMORY_PERCENT']['RADIAL'] gpu_mem_percent_text_data = theme_gpu_data['MEMORY_PERCENT']['TEXT'] @@ -467,9 +603,11 @@ def stats(cls): display_themed_progress_bar(gpu_mem_percent_graph_data, memory_percentage) display_themed_percent_radial_bar(gpu_mem_percent_radial_data, memory_percentage) display_themed_percent_value(gpu_mem_percent_text_data, memory_percentage) - display_themed_line_graph(gpu_mem_percent_line_graph_data, cls.last_values_gpu_mem_percentage) + display_themed_line_graph(gpu_mem_percent_line_graph_data, cls.last_values_gpu_mem_percentage[0]) - # GPU mem. absolute usage (M) + @classmethod + def _render_legacy_memory_used(cls, theme_gpu_data, memory_used_mb): + """Render legacy GPU memory used""" gpu_mem_used_text_data = theme_gpu_data['MEMORY_USED']['TEXT'] if math.isnan(memory_used_mb): memory_used_mb = 0 @@ -484,7 +622,9 @@ def stats(cls): unit=" M" ) - # GPU mem. total memory (M) + @classmethod + def _render_legacy_memory_total(cls, theme_gpu_data, total_memory_mb): + """Render legacy GPU total memory""" gpu_mem_total_text_data = theme_gpu_data['MEMORY_TOTAL']['TEXT'] if math.isnan(total_memory_mb): total_memory_mb = 0 @@ -495,11 +635,13 @@ def stats(cls): display_themed_value( theme_data=gpu_mem_total_text_data, value=int(total_memory_mb), - min_size=5, # Adjust min_size as necessary for your display - unit=" M" # Assuming the unit is in Megabytes + min_size=5, + unit=" M" ) - # GPU temperature (°C) + @classmethod + def _render_legacy_temperature(cls, theme_gpu_data, temperature): + """Render legacy GPU temperature""" gpu_temp_text_data = theme_gpu_data['TEMPERATURE']['TEXT'] gpu_temp_radial_data = theme_gpu_data['TEMPERATURE']['RADIAL'] gpu_temp_graph_data = theme_gpu_data['TEMPERATURE']['GRAPH'] @@ -518,9 +660,11 @@ def stats(cls): display_themed_temperature_value(gpu_temp_text_data, temperature) display_themed_progress_bar(gpu_temp_graph_data, temperature) display_themed_temperature_radial_bar(gpu_temp_radial_data, temperature) - display_themed_line_graph(gpu_temp_line_graph_data, cls.last_values_gpu_temperature) + display_themed_line_graph(gpu_temp_line_graph_data, cls.last_values_gpu_temperature[0]) - # GPU FPS + @classmethod + def _render_legacy_fps(cls, theme_gpu_data, fps): + """Render legacy GPU FPS""" gpu_fps_text_data = theme_gpu_data['FPS']['TEXT'] gpu_fps_radial_data = theme_gpu_data['FPS']['RADIAL'] gpu_fps_graph_data = theme_gpu_data['FPS']['GRAPH'] @@ -549,9 +693,11 @@ def stats(cls): min_size=4, unit=" FPS" ) - display_themed_line_graph(gpu_fps_line_graph_data, cls.last_values_gpu_fps) + display_themed_line_graph(gpu_fps_line_graph_data, cls.last_values_gpu_fps[0]) - # GPU Fan Speed (%) + @classmethod + def _render_legacy_fan_speed(cls, theme_gpu_data, fan_percent): + """Render legacy GPU fan speed""" gpu_fan_text_data = theme_gpu_data['FAN_SPEED']['TEXT'] gpu_fan_radial_data = theme_gpu_data['FAN_SPEED']['RADIAL'] gpu_fan_graph_data = theme_gpu_data['FAN_SPEED']['GRAPH'] @@ -570,13 +716,16 @@ def stats(cls): display_themed_percent_value(gpu_fan_text_data, fan_percent) display_themed_progress_bar(gpu_fan_graph_data, fan_percent) display_themed_percent_radial_bar(gpu_fan_radial_data, fan_percent) - display_themed_line_graph(gpu_fan_line_graph_data, cls.last_values_gpu_fan_speed) + display_themed_line_graph(gpu_fan_line_graph_data, cls.last_values_gpu_fan_speed[0]) - # GPU Frequency (Ghz) + @classmethod + def _render_legacy_frequency(cls, theme_gpu_data, freq_ghz): + """Render legacy GPU frequency""" gpu_freq_text_data = theme_gpu_data['FREQUENCY']['TEXT'] gpu_freq_radial_data = theme_gpu_data['FREQUENCY']['RADIAL'] gpu_freq_graph_data = theme_gpu_data['FREQUENCY']['GRAPH'] gpu_freq_line_graph_data = theme_gpu_data['FREQUENCY']['LINE_GRAPH'] + display_themed_value( theme_data=gpu_freq_text_data, value=f'{freq_ghz:.2f}', @@ -590,13 +739,101 @@ def stats(cls): unit=" GHz", min_size=4 ) - display_themed_line_graph(gpu_freq_line_graph_data, cls.last_values_gpu_frequency) + display_themed_line_graph(gpu_freq_line_graph_data, cls.last_values_gpu_frequency[0]) + + @classmethod + def _resize_storage(cls, num_gpus): + """ Helper to initialize/resize GPU storage lists """ + logger.info(f"Resizing GPU storage for {num_gpus} GPU(s).") + default_hist_size = DEFAULT_HISTORY_SIZE + try: + default_hist_size = config.THEME_DATA['STATS']['GPU'].get('GPU0',{}).get('PERCENTAGE',{}).get('LINE_GRAPH',{}).get('HISTORY_SIZE', DEFAULT_HISTORY_SIZE) + except: + pass + + # Initialize value lists + cls.loads = [math.nan] * num_gpus + cls.memory_percentages = [math.nan] * num_gpus + cls.memory_used_mbs = [math.nan] * num_gpus + cls.total_memory_mbs = [math.nan] * num_gpus + cls.temperatures = [math.nan] * num_gpus + cls.fps_values = [-1] * num_gpus + cls.fan_percents = [math.nan] * num_gpus + cls.frequencies_ghz = [math.nan] * num_gpus + cls.gpu_names = [""] * num_gpus # Initialize GPU names array + + # Initialize history lists + cls.last_values_gpu_percentage = [last_values_list(default_hist_size) for _ in range(num_gpus)] + cls.last_values_gpu_mem_percentage = [last_values_list(default_hist_size) for _ in range(num_gpus)] + cls.last_values_gpu_temperature = [last_values_list(default_hist_size) for _ in range(num_gpus)] + cls.last_values_gpu_fps = [last_values_list(default_hist_size) for _ in range(num_gpus)] + cls.last_values_gpu_fan_speed = [last_values_list(default_hist_size) for _ in range(num_gpus)] + cls.last_values_gpu_frequency = [last_values_list(default_hist_size) for _ in range(num_gpus)] + + + @classmethod + def _update_history(cls, gpu_index): + """ Helper to update history for a specific GPU """ + if gpu_index >= len(cls.last_values_gpu_percentage): + return # Safety check + + hist_size = len(cls.last_values_gpu_percentage[gpu_index]) # Get size from list itself + + save_last_value(cls.loads[gpu_index], cls.last_values_gpu_percentage[gpu_index], hist_size) + save_last_value(cls.memory_percentages[gpu_index], cls.last_values_gpu_mem_percentage[gpu_index], hist_size) + save_last_value(cls.temperatures[gpu_index], cls.last_values_gpu_temperature[gpu_index], hist_size) + save_last_value(float(cls.fps_values[gpu_index]), cls.last_values_gpu_fps[gpu_index], hist_size) + save_last_value(cls.fan_percents[gpu_index], cls.last_values_gpu_fan_speed[gpu_index], hist_size) + save_last_value(cls.frequencies_ghz[gpu_index], cls.last_values_gpu_frequency[gpu_index], hist_size) + + @classmethod + def _render_gpu_stat(cls, gpu_theme_section, stat_key, value, history_list, text_func, radial_func): + """ Helper to render common stat types """ + # Method remains unchanged + if stat_key in gpu_theme_section: + theme_def = gpu_theme_section[stat_key] + if theme_def: # Check if theme definition exists + if 'TEXT' in theme_def: + text_func(theme_def.get('TEXT',{}), value) + if 'GRAPH' in theme_def: + display_themed_progress_bar(theme_def.get('GRAPH',{}), value) + if 'RADIAL' in theme_def: + radial_func(theme_def.get('RADIAL',{}), value) + if 'LINE_GRAPH' in theme_def: + display_themed_line_graph(theme_def.get('LINE_GRAPH',{}), history_list) + + @classmethod + def _render_gpu_stat_custom_format(cls, gpu_theme_section, stat_key, value, unit, min_size, format_str='{}', history_list=None): + """ Helper to render stats requiring specific formatting """ + if stat_key in gpu_theme_section: + theme_def = gpu_theme_section[stat_key] # Check if theme definition exists + + if theme_def: + is_nan_value = value is None or math.isnan(value) + display_value = 0 if is_nan_value else value + + try: # Attempt to format the value + formatted_val_str = "N/A" if is_nan_value else format_str.format(value) + except: + formatted_val_str = "N/A" + + if 'TEXT' in theme_def: + display_themed_value(theme_def.get('TEXT',{}), formatted_val_str, unit=unit, min_size=min_size) + if 'GRAPH' in theme_def: + display_themed_progress_bar(theme_def.get('GRAPH',{}), display_value) + if 'RADIAL' in theme_def: + radial_text = "N/A" + if not is_nan_value: # Only format if value is valid + radial_text = f"{formatted_val_str}{unit}" if theme_def.get('RADIAL',{}).get("SHOW_UNIT", True) else formatted_val_str + display_themed_radial_bar(theme_def.get('RADIAL',{}), display_value, custom_text=radial_text) # Pass the potentially 0 value + if 'LINE_GRAPH' in theme_def and history_list is not None: + # Note: save_last_value already handles appending NaN to history + display_themed_line_graph(theme_def.get('LINE_GRAPH',{}), history_list) @staticmethod def is_available(): return sensors.Gpu.is_available() - class Memory: last_values_memory_swap = [] last_values_memory_virtual = [] @@ -940,4 +1177,4 @@ def stats(cls): unit="ms", min_size=6 ) - display_themed_line_graph(theme_data['LINE_GRAPH'], cls.last_values_ping) + display_themed_line_graph(theme_data['LINE_GRAPH'], cls.last_values_ping) \ No newline at end of file diff --git a/res/themes/MultiGPUTest_3.5/background.jpg b/res/themes/MultiGPUTest_3.5/background.jpg new file mode 100644 index 00000000..18e5a189 Binary files /dev/null and b/res/themes/MultiGPUTest_3.5/background.jpg differ diff --git a/res/themes/MultiGPUTest_3.5/theme.yaml b/res/themes/MultiGPUTest_3.5/theme.yaml new file mode 100644 index 00000000..7e4c383a --- /dev/null +++ b/res/themes/MultiGPUTest_3.5/theme.yaml @@ -0,0 +1,377 @@ +# res/themes/MultiGPUTest/theme.yaml +# Multi-GPU 기능 테스트를 위한 개선된 가독성의 테마. +# GPU 0 및 GPU 1의 텍스트 Value과 Name을 표시합니다. +--- +author: "sanghyunna" + +display: + DISPLAY_SIZE: 3.5" + DISPLAY_ORIENTATION: portrait + DISPLAY_RGB_LED: 0, 0, 0 + +static_images: + BACKGROUND: + PATH: background.jpg + X: 0 + Y: 0 + WIDTH: 320 + HEIGHT: 480 + +static_text: + # --- GPU 0 Label --- + GPU0_TITLE: + TEXT: "GPU 0 Stats:" + X: 10 + Y: 10 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 14 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_NAME_LBL: # *** Name Label *** + TEXT: " Name :" + X: 10 + Y: 35 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_LOAD_LBL: + TEXT: " Load :" + X: 10 + Y: 55 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_MEM_LBL: + TEXT: " Memory (%) :" + X: 10 + Y: 75 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_TEMP_LBL: + TEXT: " Temp (°C) :" + X: 10 + Y: 95 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_FAN_LBL: + TEXT: " Fan (%) :" + X: 10 + Y: 115 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_FREQ_LBL: + TEXT: " Freq (GHz) :" + X: 10 + Y: 135 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU0_FPS_LBL: + TEXT: " FPS :" + X: 10 + Y: 155 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + + # --- GPU 1 Label --- + GPU1_TITLE: + TEXT: "GPU 1 Stats:" + X: 10 + Y: 200 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 14 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_NAME_LBL: # *** Name Label *** + TEXT: " Name :" + X: 10 + Y: 225 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_LOAD_LBL: + TEXT: " Load :" + X: 10 + Y: 245 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_MEM_LBL: + TEXT: " Memory (%) :" + X: 10 + Y: 265 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_TEMP_LBL: + TEXT: " Temp (°C) :" + X: 10 + Y: 285 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_FAN_LBL: + TEXT: " Fan (%) :" + X: 10 + Y: 305 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_FREQ_LBL: + TEXT: " Freq (GHz) :" + X: 10 + Y: 325 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + GPU1_FPS_LBL: + TEXT: " FPS :" + X: 10 + Y: 345 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.jpg + ALIGN: left + ANCHOR: lt + +STATS: + GPU: + INTERVAL: 1 + + # --- GPU 0 Value --- + GPU0: + NAME: # *** Name Value *** + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 35 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + WIDTH: 180 + HEIGHT: 15 + ALIGN: left + ANCHOR: lt + PERCENTAGE: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 55 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + MEMORY_PERCENT: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 75 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + TEMPERATURE: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 95 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + FAN_SPEED: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 115 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + FREQUENCY: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 135 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + FPS: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 155 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 4 + ALIGN: left + ANCHOR: lt + + # --- GPU 1 Value --- + GPU1: + NAME: # *** Name Value *** + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 225 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + WIDTH: 180 + HEIGHT: 15 + ALIGN: left + ANCHOR: lt + PERCENTAGE: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 245 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + MEMORY_PERCENT: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 265 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + TEMPERATURE: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 285 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + FAN_SPEED: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 305 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + FREQUENCY: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 325 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 5 + ALIGN: left + ANCHOR: lt + FPS: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 130 + Y: 345 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 12 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.jpg + MIN_SIZE: 4 + ALIGN: left + ANCHOR: lt \ No newline at end of file diff --git a/res/themes/default.yaml b/res/themes/default.yaml index a915bcb6..3a0a4924 100644 --- a/res/themes/default.yaml +++ b/res/themes/default.yaml @@ -54,7 +54,72 @@ STATS: LINE_GRAPH: SHOW: False GPU: - INTERVAL: 0 + INTERVAL: 0 # Interval applies to fetching data for ALL GPUs + + # --- Multi-GPU Structure Defaults --- + GPU0: # Default structure for the first detected GPU (index 0) + PERCENTAGE: + GRAPH: + SHOW: False + RADIAL: + SHOW: False + TEXT: + SHOW: False + LINE_GRAPH: + SHOW: False + MEMORY_PERCENT: + GRAPH: + SHOW: False + RADIAL: + SHOW: False + TEXT: + SHOW: False + LINE_GRAPH: + SHOW: False + MEMORY_USED: + TEXT: + SHOW: False + MEMORY_TOTAL: + TEXT: + SHOW: False + TEMPERATURE: + TEXT: + SHOW: False + GRAPH: + SHOW: False + RADIAL: + SHOW: False + LINE_GRAPH: + SHOW: False + FPS: + TEXT: + SHOW: False + GRAPH: + SHOW: False + RADIAL: + SHOW: False + LINE_GRAPH: + SHOW: False + FAN_SPEED: + TEXT: + SHOW: False + GRAPH: + SHOW: False + RADIAL: + SHOW: False + LINE_GRAPH: + SHOW: False + FREQUENCY: + TEXT: + SHOW: False + GRAPH: + SHOW: False + RADIAL: + SHOW: False + LINE_GRAPH: + SHOW: False + + # --- Single-GPU Structure Defaults (kept for backwards compatibility) --- PERCENTAGE: GRAPH: SHOW: False @@ -122,6 +187,7 @@ STATS: SHOW: False LINE_GRAPH: SHOW: False + MEMORY: INTERVAL: 0 SWAP: @@ -139,11 +205,14 @@ STATS: LINE_GRAPH: SHOW: False USED: - SHOW: False + TEXT: + SHOW: False FREE: - SHOW: False + TEXT: + SHOW: False TOTAL: - SHOW: False + TEXT: + SHOW: False PERCENT_TEXT: SHOW: False DISK: @@ -245,4 +314,4 @@ STATS: TEXT: SHOW: False CUSTOM: - INTERVAL: 0 + INTERVAL: 0 \ No newline at end of file