# tray.py import threading from threading import Event from pystray import Icon, MenuItem, Menu from PIL import Image, ImageDraw, ImageFont #from functools import partial import os import sys import time import psutil import pythoncom import wmi #import ctypes import pynvml from pynvml import * # from pynvml import nvmlDeviceGetTemperature, nvmlDeviceGetTemperatureThreshold, NVML_TEMPERATURE_GPU, NVML_TEMPERATURE_THRESHOLD_GPU_MAX, nvmlInit, nvmlShutdown, nvmlDeviceGetHandleByIndex, nvmlDeviceGetUtilizationRates, nvmlDeviceGetMemoryInfo, nvmlDeviceGetCount, nvmlDeviceGetName from difflib import get_close_matches import subprocess # Importieren Sie subprocess import tempfile import shutil icons = {} current_colors = {} last_colors = {} stop_events = {} color_lock = threading.Lock() icon_lock = threading.Lock() COLOR_MAP = { "gray": (160, 160, 160, 255), "green": (0, 128, 0, 255), "red": (128, 0, 0, 255), "yellow": (220, 220, 0, 255) } def managed_thread(target, *args, **kwargs): """ Führt target(*args, **kwargs) in Schleife aus, bis shutdown_event gesetzt ist. Ideal für Monitoring-Loops. """ def wrapper(): while not shutdown_event.is_set(): target(*args, **kwargs) time.sleep(2.1) # oder individuell einstellbar t = threading.Thread(target=wrapper, daemon=True) thread_refs.append(t) t.start() shutdown_event = Event() thread_refs = [] shutdown_requested = threading.Event() def resource_path(relative_path): try: base_path = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) full_path = os.path.join(base_path, relative_path) if not os.path.exists(full_path) and hasattr(sys, 'frozen'): raise FileNotFoundError # Trigger fallback return full_path except (AttributeError, FileNotFoundError): try: import pkgutil data = pkgutil.get_data(__name__, relative_path) if data: temp_dir = tempfile.gettempdir() temp_file = os.path.join(temp_dir, os.path.basename(relative_path)) with open(temp_file, 'wb') as f: f.write(data) return temp_file except Exception as e: print(f"[ERROR] Fallback-Resource-Pfad fehlgeschlagen: {e}") # Last resort: use relative path from package root pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "your_package_name")) fallback_path = os.path.join(pkg_root, relative_path) if os.path.exists(fallback_path): return fallback_path raise FileNotFoundError(f"Resource '{relative_path}' not found in any path.") font_path = resource_path("DePixelSchmal.otf") def round_to_nearest_five(value): return int(round(value / 5.0) * 5) def get_active_network_adapters(): c = wmi.WMI() adapters = [] for nic in c.Win32_NetworkAdapterConfiguration(IPEnabled=True): if hasattr(nic, 'Description'): adapters.append(nic.Description) return adapters def get_adapter_speeds(): c = wmi.WMI() speeds = {} for nic in c.Win32_NetworkAdapter(): if nic.NetEnabled and nic.Speed: speeds[nic.Name] = int(nic.Speed) # Bits per second return speeds def find_best_match(name, candidates): matches = get_close_matches(name, candidates, n=1, cutoff=0.6) return matches[0] if matches else None def create_text_icon(text, color=(255, 255, 255, 255), bg_color=(0, 0, 0, 0)): size = 77 image = Image.new("RGBA", (size, size), bg_color) draw = ImageDraw.Draw(image) try: font = ImageFont.truetype(font_path, 29) except Exception: font = ImageFont.load_default() bbox = draw.textbbox((0, 0), text, font=font) if hasattr(draw, 'textbbox') else font.getsize(text) text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1] text_x = (size - text_w) // 2 - bbox[0] text_y = (size - text_h) // 2 - bbox[1] draw.text((text_x, text_y), text, font=font, fill=color) return image def format_speed_custom(value_kb): units = ['kB/s', 'MB/s', 'GB/s'] speed = value_kb unit_index = 0 while speed >= 100 and unit_index < len(units) - 1: speed /= 1024 unit_index += 1 if speed < 10: display = f"{speed:.1f}" else: display = f"{min(round(speed), 99)}" return f"{display}\n{units[unit_index]}" # drive icon def get_color(read_active, write_active, read_mb=0, write_mb=0): if read_mb < 2 and write_mb < 2: return "gray" elif read_mb >= 2 and write_mb >= 2: ratio = read_mb / write_mb if write_mb != 0 else float('inf') if 1/5 <= ratio <= 5: return "yellow" elif write_mb > read_mb: return "red" else: return "green" elif write_mb >= 2: return "red" elif read_mb >= 2: return "green" return "gray" def _set_icon_color(key, color): with icon_lock: icon_data = icons.get(key) if icon_data: icon = icon_data["icon"] label = icon_data["label"] new_icon = create_icon(COLOR_MAP.get(color, (128, 128, 128)), label) try: icon.icon = new_icon except Exception as e: print(f"[WARN] Could not update icon for {key}: {e}") finally: with color_lock: last_colors[key] = color # Drive letter ICONS def create_icon(color_rgb, label): size = 77 image = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) draw.ellipse((0, 0, size, size), fill=color_rgb) try: font = ImageFont.truetype(font_path, 55) except Exception: font = ImageFont.load_default() if hasattr(draw, 'textbbox'): bbox = draw.textbbox((0, 0), label, font=font) else: width, height = font.getsize(label) bbox = (0, 0, width, height) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1] text_x = (size - text_w) / 2 text_y = (size - text_h) / 2 - bbox[1] brightness = sum(color_rgb) / 3 text_color = (0, 0, 0, 255) if brightness > 130 else (255, 255, 255, 255) draw.text((text_x, text_y), label, font=font, fill=text_color) return image def update_tray_color(key, color): with color_lock: old_color = current_colors.get(key) if old_color == color: return current_colors[key] = color def _icon_updater(key, stop_event): while not stop_event.is_set(): with color_lock: color = current_colors.get(key, "gray") last_color = last_colors.get(key) if color != last_color: try: _set_icon_color(key, color) except Exception as e: print(f"[WARN] Icon update failed for {key}: {e}") stop_event.wait(0.2) # gradient color bar ICON: cpu, ram, gpu, vram, temp def get_gradient_color(percent): """ Gibt eine Farbe für den gegebenen Prozentwert aus einem Regenbogenverlauf zurück: 0% -> grün, 50% -> gelb, 100% -> rot """ if percent <= 50: # Grün → Gelb ratio = percent / 50.0 r = int(COLOR_MAP["green"][0] + ratio * (COLOR_MAP["yellow"][0] - COLOR_MAP["green"][0])) g = int(COLOR_MAP["green"][1] + ratio * (COLOR_MAP["yellow"][1] - COLOR_MAP["green"][1])) b = int(COLOR_MAP["green"][2] + ratio * (COLOR_MAP["yellow"][2] - COLOR_MAP["green"][2])) else: # Gelb → Rot ratio = (percent - 50) / 50.0 r = int(COLOR_MAP["yellow"][0] + ratio * (COLOR_MAP["red"][0] - COLOR_MAP["yellow"][0])) g = int(COLOR_MAP["yellow"][1] + ratio * (COLOR_MAP["red"][1] - COLOR_MAP["yellow"][1])) b = int(COLOR_MAP["yellow"][2] + ratio * (COLOR_MAP["red"][2] - COLOR_MAP["yellow"][2])) return (r, g, b, 255) def create_bar_icon(percent, label, color=None): """ Erstellt ein Balken-Icon mit Regenbogen-Farbverlauf. 0% → unten grün, 50% → mitte gelb, 100% → oben rot """ size = 77 margin = 4 image = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) # Balkengröße bar_width = size - 2 * margin bar_height = int((percent / 100.0) * size) bar_x0 = margin bar_x1 = size - margin bar_y_bottom = size - 1 bar_y_top = bar_y_bottom - bar_height + 1 # Farbverlauf zeichnen for i in range(bar_height): rel_percent = (i / bar_height) * percent line_color = get_gradient_color(rel_percent) y = bar_y_bottom - i draw.line([(bar_x0, y), (bar_x1, y)], fill=line_color) # Label zeichnen try: font = ImageFont.truetype(font_path, 30) except: font = ImageFont.load_default() draw.text((2, 2), label, font=font, fill=(255, 255, 255, 255)) return image def _on_quit(icon_inst=None, item=None): print("[INFO] Beenden eingeleitet...") # 1. Stop-Flag setzen und Threads sauber beenden shutdown_event.set() for t in thread_refs: t.join(timeout=2.1) print("[INFO] Alle Threads beendet.") # 2. Tray-Icons stoppen stop_all_tray_icons() print("[INFO] Alle Trays beendet.") # 3. pynvml sauber beenden try: pynvml.nvmlShutdown() except Exception: pass print("[INFO] Nvidia shut down.") time.sleep(0.1) shutdown_requested.set() def _on_restart(icon_inst=None, item=None): print("[INFO] Neustart eingeleitet...") # 1. Stop-Flag setzen und Threads sauber beenden shutdown_event.set() for t in thread_refs: t.join(timeout=2.1) print("[INFO] Alle Threads beendet.") # 2. Tray-Icons stoppen stop_all_tray_icons() # 3. pynvml sauber beenden try: pynvml.nvmlShutdown() except Exception: pass time.sleep(0.1) base_dir = os.path.dirname(os.path.abspath(__file__)) startdir_path = os.path.join(base_dir, "startdir.txt") print("[INFO] Start Folder lesen") try: with open(startdir_path, "r", encoding="utf-8") as f: executable_path = f.readline().strip() if not os.path.isfile(executable_path): raise FileNotFoundError(f"EXE nicht gefunden: {executable_path}") except Exception as e: print(f"[ERROR] Fehler beim Lesen der startdir.txt: {e}") if icon_inst: icon_inst.stop() return exe_dir = os.path.dirname(executable_path) time.sleep(0.1) try: print(f"[INFO] Starte EXE erneut (via subprocess.Popen): {executable_path}") env = os.environ.copy() env["RESTART_COUNT"] = str(int(env.get("RESTART_COUNT", "0")) + 1) env["PYINSTALLER_RESET_ENVIRONMENT"] = "1" subprocess.Popen( [executable_path], cwd=exe_dir, env=env, close_fds=True, shell=False ) print(f"[INFO] EXE gestartet!") except Exception as e: print(f"[ERROR] Fehler beim Start via Popen: {e}") return print("[INFO] Old Instance EXIT") time.sleep(0.5) shutdown_requested.set() def stop_all_tray_icons(): for icon in icons.values(): try: if icon["icon"].visible: icon["icon"].visible = False icon["icon"].stop() except Exception as e: print(f"[WARN] Icon-Stop fehlgeschlagen: {e}") for event in stop_events.values(): event.set() def update_tray_tooltip(key, tooltip_text): icon_data = icons.get(key) if icon_data: icon = icon_data["icon"] try: icon.title = tooltip_text[:127] except Exception as e: print(f"[WARN] Tooltip konnte nicht gesetzt werden für {key}: {e}") def update_net_icons(adapter_name, send_kb, recv_kb, selected_components): menu = Menu( MenuItem("Restart", lambda icon_inst, item: _on_restart(icon_inst, item)), MenuItem("Exit", lambda icon_inst, item: _on_quit(icon_inst, item)) ) def update_icon(direction, value_kb): if not selected_components['network']: return if value_kb < 10: value_kb = 0 key = f"NET_{adapter_name}, Direction: {direction}" text_with_linebreak = f"{'U ' if direction == 'SEND' else 'D '}{format_speed_custom(value_kb)}" speed_parts = format_speed_custom(value_kb).split("\n") text_no_linebreak = f"{speed_parts[0]} {speed_parts[1]}" if len(speed_parts) == 2 else format_speed_custom(value_kb) image = create_text_icon(text_with_linebreak) if key not in icons: icon = Icon(key, image, menu=menu) icons[key] = {"icon": icon, "label": key} print(f"Created Network icon {key}") icon.run_detached() else: icons[key]["icon"].icon = image # Set tooltip using the update_tray_tooltip function tooltip = f"{adapter_name} {'Upload' if direction == 'SEND' else 'Download'}: {text_no_linebreak}" tooltip = tooltip[:127] # falls percpu=True (tooltips max128 zeichen) update_tray_tooltip(key, tooltip) threading.Thread(target=update_icon, args=("SEND", send_kb), daemon=True).start() threading.Thread(target=update_icon, args=("RECV", recv_kb), daemon=True).start() def sort_selected_drives(drive_selections, device_map): items = [(dev, part) for dev, parts in device_map.items() for part in parts] # Zugriff auf 'letter' für Sortierung sorted_items = sorted(items, key=lambda x: x[1]['letter'].upper(), reverse=True) # Auch drive_selections sortieren anhand des zweiten Elements (Laufwerksbuchstabe) return sorted(drive_selections, key=lambda x: x[1].upper(), reverse=True) def start_drive_icons(hardware_info, stop_all_tray_icons, device_map, drive_selections): print("Starting tray icons for selected drives...") menu = Menu( MenuItem("Restart", lambda icon_inst, item: _on_restart(icon_inst, item)), MenuItem("Exit", lambda icon_inst, item: _on_quit(icon_inst, item)) ) sorted_drive_selections = sort_selected_drives(drive_selections, device_map) for index, (dev, part) in enumerate(sorted_drive_selections): icon_label = part.strip(":") icon_key = f"{dev}_{part}" icon_title = f"{index}_SmartTaskTool_{icon_label}" image = create_icon(COLOR_MAP["gray"], icon_label) icon = Icon(icon_title, image, menu=menu) icons[icon_key] = {"icon": icon, "label": icon_label} current_colors[icon_key] = "gray" last_colors[icon_key] = None stop_event = threading.Event() stop_events[icon_key] = stop_event icon.run_detached() threading.Thread(target=_icon_updater, args=(icon_key, stop_event), daemon=True).start() time.sleep(0.2) print("Started Icons:") for key, value in icons.items(): print(f" {key}: {value}") def start_tray_monitoring(hardware_info, selected_components): if not isinstance(selected_components, dict): raise ValueError("selected_components muss ein Dictionary sein") expected_keys = ['cpu', 'ram', 'gpu', 'network', 'drives'] # Check if all expected keys are present and have the correct type for key in expected_keys: if key not in selected_components: raise KeyError(f"selected_components fehlt: '{key}'") value = selected_components[key] if key == 'drives': if not isinstance(value, list): raise TypeError(f"'{key}' muss eine Liste sein.") if not all(isinstance(item, tuple) and len(item) == 2 for item in value): raise ValueError(f"Jedes Element in '{key}' muss ein Tuple mit zwei Elementen sein.") else: if not isinstance(value, bool): raise TypeError(f"'{key}' muss ein Boolean sein.") print("Starting tray monitoring...") menu = Menu( MenuItem("Restart", lambda icon_inst, item: _on_restart(icon_inst, item)), MenuItem("Exit", lambda icon_inst, item: _on_quit(icon_inst, item)) ) def update_cpu(percent): # Dummy-Wert, tatsächliche Auswertung erfolgt kontrolliert if not selected_components['cpu']: return # CPU-Werte kontrolliert ermitteln try: logical = psutil.cpu_count(logical=True) physical = psutil.cpu_count(logical=False) cpu_percentages = psutil.cpu_percent(interval=None, percpu=True) num_cores = logical if logical else physical core_usages = cpu_percentages[:num_cores] if not core_usages: raise ValueError("Keine CPU-Werte erhalten.") avg_cpu_percent = sum(core_usages) / len(core_usages) percent = round_to_nearest_five(avg_cpu_percent) except Exception as e: print(f"[ERROR] CPU-Auswertung fehlgeschlagen: {e}") percent = 0 core_usages = [] num_cores = 0 # Icon-Update image = create_bar_icon(percent, "CPU") key = "CPU_USAGE" if key not in icons: icon = Icon(key, image, menu=menu) icons[key] = {"icon": icon, "label": "CPU"} print(f"Created CPU icon") threading.Thread(target=icon.run, daemon=True).start() else: icons[key]["icon"].icon = image # Tooltip try: cores_str = " | ".join([f"{int(p)}%" for p in core_usages]) tooltip = f"{num_cores} Cores: {cores_str}" tooltip = tooltip[:127] update_tray_tooltip(key, tooltip) except Exception: tooltip = f"CPU {percent}%" #icons[key]["icon"].title = tooltip def update_ram(percent): if not selected_components['ram']: return percent = round_to_nearest_five(percent) image = create_bar_icon(percent, "RAM") key = "RAM_USAGE" if key not in icons: icon = Icon(key, image, menu=menu) icons[key] = {"icon": icon, "label": "RAM"} print(f"Created RAM icon") threading.Thread(target=icon.run, daemon=True).start() else: icons[key]["icon"].icon = image try: mem = psutil.virtual_memory() used_gb = round(mem.used / (1024 ** 3)) total_gb = round(mem.total / (1024 ** 3)) tooltip = f"RAM {used_gb} / {total_gb} GB" update_tray_tooltip(key, tooltip) except Exception: tooltip = f"RAM: {percent}%" #icons[key]["icon"].title = tooltip def update_gpu_vram_temp(idx, util, mem_used, mem_total, temp, max_temp): if not selected_components['gpu']: return # === Temperatur === key_temp = f"GPU{idx}_TEMP" label_temp = f"T{idx}" temp_rounded = round(temp) # Temperatur in Prozent (dynamisch zu max_temp) clamped = max(35, min(temp, max_temp)) # untere Grenze: 35°C pct = round((clamped - 35) / (max_temp - 35) * 100) image_temp = create_bar_icon(pct, label_temp) if key_temp not in icons: try: icon = Icon(key_temp, image_temp, menu=menu) icons[key_temp] = {"icon": icon, "label": label_temp} print(f"Created GPU{idx} temperature icon.") threading.Thread(target=icon.run, daemon=True).start() except Exception as e: print(f"Error creating GPU{idx} temperature icon: {e}") else: icons[key_temp]["icon"].icon = image_temp tooltip = f"{label_temp}: {temp_rounded} °C / {max_temp} °C" tooltip = tooltip[:127] update_tray_tooltip(key_temp, tooltip) #icons[key_temp]["icon"].title = f"{label_temp}: {temp_rounded} °C / {max_temp} °C" time.sleep(0.01) # === VRAM-Nutzung === key_vram = f"VRAM{idx}_USAGE" label_vram = f"VR{idx}" vram_util = round_to_nearest_five(mem_used / mem_total * 100) image_vram = create_bar_icon(vram_util, label_vram) if key_vram not in icons: try: icon = Icon(key_vram, image_vram, menu=menu) icons[key_vram] = {"icon": icon, "label": label_vram} print(f"Created VRAM{idx} usage icon.") threading.Thread(target=icon.run, daemon=True).start() except Exception as e: print(f"Error creating VRAM{idx} usage icon: {e}") else: icons[key_vram]["icon"].icon = image_vram used_gb = round(mem_used / (1024**3)) total_gb = round(mem_total / (1024**3)) # Set tooltip using the update_tray_tooltip function tooltip = f"{label_vram}: {used_gb}/{total_gb} GB" tooltip = tooltip[:127] update_tray_tooltip(key_vram, tooltip) #icons[key_vram]["icon"].title = f"{label_vram}: {used_gb}/{total_gb} GB" time.sleep(0.01) # === GPU-Nutzung === key_gpu = f"GPU{idx}_USAGE" label_gpu = f"GPU{idx}" image_gpu = create_bar_icon(util, label_gpu) if key_gpu not in icons: try: icon = Icon(key_gpu, image_gpu, menu=menu) icons[key_gpu] = {"icon": icon, "label": label_gpu} print(f"Created GPU{idx} usage icon.") threading.Thread(target=icon.run, daemon=True).start() except Exception as e: print(f"Error creating GPU{idx} usage icon: {e}") else: icons[key_gpu]["icon"].icon = image_gpu #icons[key_gpu]["icon"].title = f"{label_gpu}: {util}%" # Set tooltip using the update_tray_tooltip function tooltip = f"{label_gpu}: {util}%" tooltip = tooltip[:127] update_tray_tooltip(key_gpu, tooltip) def gpu_monitor(): try: pynvml.nvmlInit() gpu_data = [] for idx in range(pynvml.nvmlDeviceGetCount()): handle = pynvml.nvmlDeviceGetHandleByIndex(idx) try: max_temp = pynvml.nvmlDeviceGetTemperatureThreshold( handle, pynvml.NVML_TEMPERATURE_THRESHOLD_GPU_MAX ) except pynvml.NVMLError: max_temp = 90 # Fallback gpu_data.append((idx, handle, max_temp)) while not shutdown_event.is_set(): #while True: for idx, handle, max_temp in gpu_data: try: util = pynvml.nvmlDeviceGetUtilizationRates(handle).gpu mem = pynvml.nvmlDeviceGetMemoryInfo(handle) temp = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU) update_gpu_vram_temp(idx, util, mem.used, mem.total, temp, max_temp) except pynvml.NVMLError as e: print(f"[ERROR] GPU{idx} Fehler: {e}") time.sleep(0.5) except pynvml.NVMLError as e: print(f"[ERROR] NVML Initialisierung fehlgeschlagen: {e}") def cpu_monitor(): while not shutdown_event.is_set(): try: update_cpu(psutil.cpu_percent(interval=None)) except Exception as e: print(f"[ERROR] CPU Monitoring Fehler: {e}") time.sleep(0.5) def ram_monitor(): while not shutdown_event.is_set(): try: mem = psutil.virtual_memory() update_ram(mem.percent) except Exception as e: print(f"[ERROR] RAM Monitoring Fehler: {e}") time.sleep(0.5) def wmi_monitor(poll_interval=2): # if polinterval change -> set MB/s and kb/s pythoncom.CoInitialize() c = wmi.WMI(namespace="root\\CIMV2") prev_disk_counters = {} prev_net_stats = {} speeds = get_adapter_speeds() active_adapters = get_active_network_adapters() adapter_map = {active: find_best_match(active, list(speeds.keys())) for active in active_adapters} try: next_time = time.perf_counter() while not shutdown_event.is_set(): #while True: try: # Drives perf_logical_disks = { disk.Name.upper(): disk for disk in c.Win32_PerfRawData_PerfDisk_LogicalDisk() } for dev, part in selected_components.get('drives', []): if not selected_components['drives']: continue perf_disk = perf_logical_disks.get(part) if not perf_disk: continue read_bytes = int(perf_disk.DiskReadBytesPerSec) write_bytes = int(perf_disk.DiskWriteBytesPerSec) prev_read, prev_write = prev_disk_counters.get(part, (0, 0)) read_diff = max(0, read_bytes - prev_read) write_diff = max(0, write_bytes - prev_write) prev_disk_counters[part] = (read_bytes, write_bytes) mb_read = read_diff / 1024 / 1024 / 2 mb_write = write_diff / 1024 / 1024 / 2 key = f"{dev}_{part}" color = get_color(mb_read > 2.0, mb_write > 2.0, mb_read, mb_write) update_tray_color(key, color) update_tray_tooltip(key, f"{part} R {int(mb_read)} MB/s | W {int(mb_write)} MB/s") # Network perf_net_ifaces = c.Win32_PerfRawData_Tcpip_NetworkInterface() for iface in perf_net_ifaces: iface_name = getattr(iface, 'Name', None) if not iface_name: continue adapter_name = find_best_match(iface_name, list(adapter_map.keys())) if not adapter_name or not selected_components['network']: continue send_raw = int(iface.BytesSentPersec) recv_raw = int(iface.BytesReceivedPerSec) #print(f"Raw Network Data - Send: {send_raw}, Recv: {recv_raw}") prev_send, prev_recv = prev_net_stats.get(adapter_name, (0, 0)) send_diff = max(0, send_raw - prev_send) recv_diff = max(0, recv_raw - prev_recv) #print(f"Network Diff Data - Send: {send_diff}, Recv: {recv_diff}") prev_net_stats[adapter_name] = (send_raw, recv_raw) # Convert bytes to KB send_kb = send_diff / 1024 / 2 recv_kb = recv_diff / 1024 / 2 #print(f"Converted Network Data - Send: {send_kb} KB/s, Recv: {recv_kb} KB/s") update_net_icons(adapter_name, send_kb, recv_kb, selected_components) except Exception as e: print(f"[ERROR] Unified WMI Monitor: {e}") next_time += poll_interval time.sleep(max(0, next_time - time.perf_counter())) finally: pythoncom.CoUninitialize() if selected_components['cpu']: managed_thread(cpu_monitor) time.sleep(0.3) if selected_components['ram']: managed_thread(ram_monitor) time.sleep(0.3) if selected_components['gpu']: managed_thread(gpu_monitor) time.sleep(0.3) if selected_components['network']: managed_thread(wmi_monitor) time.sleep(2) # Start tray icons for drives based on user selection if selected_components['drives']: device_map = hardware_info.get('drive_map', {}) drive_selections = selected_components['drives'] start_drive_icons(hardware_info, stop_all_tray_icons, device_map, drive_selections) print("All tray monitoring components started.")