kalle07 commited on
Commit
d5174e7
·
verified ·
1 Parent(s): f286ad1

Upload 5 files

Browse files
Files changed (5) hide show
  1. build.py +49 -0
  2. gui.py +283 -0
  3. hardware.py +142 -0
  4. main.py +115 -0
  5. tray.py +830 -0
build.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import PyInstaller.__main__
2
+ import shutil
3
+ import os
4
+
5
+ # Aufräumen vorheriger Builds
6
+ for folder in ['build', 'dist', '__pycache__']:
7
+ if os.path.exists(folder):
8
+ shutil.rmtree(folder)
9
+
10
+ # PyInstaller Optionen ohne --icon
11
+ opts = [
12
+ 'main.py',
13
+ '--name=SmartTaskTool_by_Sevenof9',
14
+ '--onefile',
15
+ #'--console',
16
+ '--noconsole',
17
+ '--windowed',
18
+ '--clean',
19
+ '--log-level=WARN',
20
+ '--add-data=gui.py;.', # gui.py in dist/ kopieren
21
+ '--add-data=tray.py;.', # tray.py in dist/ kopieren
22
+ '--add-data=hardware.py;.', # hardware.py in dist/ kopieren
23
+ '--add-data=restart_helper.py;.', # restart_helper.py in dist/ kopieren
24
+ '--add-data=DePixelSchmal.otf;.',
25
+ ]
26
+
27
+ # Hidden-Imports
28
+ hidden_imports = [
29
+ 'win32com',
30
+ 'win32com.client',
31
+ 'pythoncom',
32
+ 'pystray._win32',
33
+ 'pystray._base',
34
+ 'wmi',
35
+ 'pynvml',
36
+ 'pystray',
37
+ 'PIL.Image',
38
+ 'PIL.ImageDraw',
39
+ 'PIL.ImageFont',
40
+ 'pythoncom',
41
+ 'wx',
42
+ 'difflib',
43
+ 'psutil'
44
+ ]
45
+
46
+ for hidden in hidden_imports:
47
+ opts.append(f'--hidden-import={hidden}')
48
+
49
+ PyInstaller.__main__.run(opts)
gui.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gui.py
2
+
3
+ import wx
4
+
5
+ class MainFrame(wx.Frame):
6
+
7
+ MAX_WIDTH = 1000
8
+ MAX_HEIGHT = 1000
9
+ MIN_WIDTH = 300
10
+ MIN_HEIGHT = 200
11
+
12
+ def __init__(self, *args, hardware_info=None, result_queue=None, **kwargs):
13
+ super().__init__(*args, **kwargs)
14
+ self.hardware_info = hardware_info or {}
15
+ self.result_queue = result_queue
16
+
17
+ self.selected_components = {
18
+ 'cpu': True,
19
+ 'ram': True,
20
+ 'gpu': True,
21
+ 'network': True,
22
+ 'drives': []
23
+ }
24
+
25
+ # EVT_CLOSE binden
26
+ self.Bind(wx.EVT_CLOSE, self.on_close)
27
+
28
+ # Checkboxen speichern: Dictionary mit Kategorie als Key und Liste von Checkboxes als Value
29
+ self.checkboxes = {}
30
+
31
+ self.panel = wx.Panel(self)
32
+ vbox = wx.BoxSizer(wx.VERTICAL)
33
+
34
+ info = wx.StaticText(self.panel, label="Window will self-close in 10 sec")
35
+ vbox.Add(info, 0, wx.ALL, 10)
36
+
37
+ auto_close_info = wx.StaticText(
38
+ self.panel,
39
+ label="Drive monitoring (read/write) every second\nThreshold 2MB"
40
+ )
41
+ vbox.Add(auto_close_info, 0, wx.LEFT | wx.BOTTOM, 10)
42
+
43
+ tray_info = wx.StaticText(
44
+ self.panel,
45
+ label="(Hover over tray icon to see further information)"
46
+ )
47
+ vbox.Add(tray_info, 0, wx.LEFT | wx.BOTTOM, 10)
48
+
49
+ # CPU-Info: eine Checkbox mit zusammengesetzten Infos
50
+ if hardware_info.get('cpu_info'):
51
+ cpu_info = hardware_info['cpu_info']
52
+ cpu_label = (f"CPU Info: Logical cores: {cpu_info.get('logical_cores')}, "
53
+ f"Physical cores: {cpu_info.get('physical_cores')}, "
54
+ f"Frequency: {cpu_info.get('frequency', 'N/A')} MHz")
55
+ checkbox = wx.CheckBox(self.panel, label=cpu_label)
56
+ checkbox.SetValue(True)
57
+ vbox.Add(checkbox, 0, wx.LEFT | wx.BOTTOM, 10)
58
+ self.checkboxes['cpu'] = [checkbox]
59
+
60
+ # RAM-Info: eine Checkbox mit zusammengesetzten Infos
61
+ if hardware_info.get('ram_info'):
62
+ ram_info = hardware_info['ram_info']
63
+ ram_label = (f"RAM Info: Total: {ram_info.get('total_gb', 'N/A')} GB, "
64
+ f"Available: {ram_info.get('available_gb', 'N/A')} GB")
65
+ checkbox = wx.CheckBox(self.panel, label=ram_label)
66
+ checkbox.SetValue(True)
67
+ vbox.Add(checkbox, 0, wx.LEFT | wx.BOTTOM, 10)
68
+ self.checkboxes['ram'] = [checkbox]
69
+
70
+ # GPU-Info
71
+ if hardware_info.get('gpu_info'):
72
+ self.add_section(vbox, "GPU Info:", [
73
+ f"{gpu['name']} ({gpu['memory_total_mb']} MB VRAM / MaxTemp: {gpu['max_temp']} °C)"
74
+ for gpu in hardware_info['gpu_info']
75
+ ], category="gpu")
76
+
77
+ # Netzwerkadapter
78
+ if hardware_info.get('network_adapters'):
79
+ self.add_section(vbox, "Active Network Adapters:", hardware_info['network_adapters'], category="network")
80
+
81
+ # Drive-Info
82
+ if hardware_info.get('drive_map'):
83
+ self.add_drive_section(vbox)
84
+
85
+ # Submit Button
86
+ self.submit_button = wx.Button(self.panel, label="Submit and Close (10)")
87
+ self.submit_button.Bind(wx.EVT_BUTTON, self.on_submit)
88
+ vbox.Add(self.submit_button, 0, wx.ALL | wx.CENTER, 10)
89
+
90
+ self.panel.SetSizer(vbox)
91
+ self.panel.Layout()
92
+ self.submit_button.SetFocus()
93
+ self.SetDefaultItem(self.submit_button)
94
+
95
+ best_size = self.panel.GetBestSize()
96
+ width = min(max(best_size.width, self.MIN_WIDTH), self.MAX_WIDTH)
97
+ height = min(max(best_size.height, self.MIN_HEIGHT), self.MAX_HEIGHT)
98
+ self.SetClientSize((width, height))
99
+ self.SetPosition((200,100))
100
+ self.SetTitle("SmartTaskTool by Sevenof9")
101
+
102
+ self.countdown_timer = 10
103
+ self.timer = wx.Timer(self)
104
+ self.Bind(wx.EVT_TIMER, self.update_countdown, self.timer)
105
+ self.timer.Start(1000) # Update every second
106
+
107
+ def update_countdown(self, event):
108
+ self.countdown_timer -= 1
109
+ if self.countdown_timer <= 0:
110
+ self.submit_values()
111
+ self.timer.Stop()
112
+ else:
113
+ self.submit_button.SetLabel(f"Submit and Close ({self.countdown_timer})")
114
+
115
+ def add_drive_section(self, vbox):
116
+ drive_map = self.hardware_info.get('drive_map', {})
117
+ vbox.Add(wx.StaticText(self.panel, label="Detected Drives:"), 0, wx.LEFT | wx.TOP, 10)
118
+ for dev in sorted(drive_map.keys()):
119
+ parts = drive_map[dev]
120
+ for part_info in sorted(parts, key=lambda x: x['letter']):
121
+ letter = part_info.get('letter', 'N/A')
122
+ label = part_info.get('label', 'N/A')
123
+ checkbox_label = f"{dev} - {letter} ({label})"
124
+ checkbox = wx.CheckBox(self.panel, label=checkbox_label)
125
+ checkbox.SetValue(True)
126
+ vbox.Add(checkbox, 0, wx.LEFT | wx.BOTTOM, 10)
127
+
128
+ if 'drives' not in self.checkboxes:
129
+ self.checkboxes['drives'] = []
130
+ self.checkboxes['drives'].append((dev, letter, checkbox))
131
+
132
+ def submit_values(self):
133
+ selected_components = {
134
+ 'cpu': False,
135
+ 'ram': False,
136
+ 'gpu': False,
137
+ 'network': False,
138
+ 'drives': []
139
+ }
140
+
141
+ checkbox_categories = ['cpu', 'ram', 'gpu', 'network']
142
+ for category in checkbox_categories:
143
+ if category in self.checkboxes:
144
+ selected_components[category] = any(checkbox.GetValue() for checkbox in self.checkboxes[category])
145
+
146
+ if 'drives' in self.checkboxes:
147
+ for dev, part, checkbox in self.checkboxes['drives']:
148
+ if checkbox.GetValue():
149
+ selected_components['drives'].append((dev, part))
150
+
151
+ # Hardwareinfo ergänzen
152
+ selected_components['cpu_info'] = self.hardware_info.get('cpu_info', {})
153
+ selected_components['ram_info'] = self.hardware_info.get('ram_info', {})
154
+ selected_components['gpu_info'] = self.hardware_info.get('gpu_info', [])
155
+ selected_components['network_adapters'] = self.hardware_info.get('network_adapters', [])
156
+ selected_components['drive_map'] = self.hardware_info.get('drive_map', {})
157
+
158
+ print("[DEBUG] Auswahl:", selected_components)
159
+ if self.result_queue:
160
+ self.result_queue.put(selected_components)
161
+ self.Close()
162
+
163
+ def on_submit(self, event):
164
+ print("[DEBUG] Submit-Button geklickt")
165
+ self.submit_values()
166
+
167
+ def add_section(self, vbox, title, items, category):
168
+ vbox.Add(wx.StaticText(self.panel, label=title), 0, wx.LEFT | wx.TOP, 10)
169
+ for item in items:
170
+ checkbox = wx.CheckBox(self.panel, label=item)
171
+ checkbox.SetValue(True)
172
+ vbox.Add(checkbox, 0, wx.LEFT | wx.BOTTOM, 10)
173
+
174
+ if category not in self.checkboxes:
175
+ self.checkboxes[category] = []
176
+
177
+ if category == "drives":
178
+ # Assuming dev and part are extracted from item
179
+ parts = item.split(":")
180
+ if len(parts) == 2:
181
+ dev, part = parts
182
+ self.checkboxes[category].append((dev.strip(), part.strip(), checkbox))
183
+ else:
184
+ self.checkboxes[category].append(checkbox)
185
+
186
+ def get_selected_components(self):
187
+ selected = {
188
+ 'cpu': False,
189
+ 'ram': False,
190
+ 'gpu': False,
191
+ 'network': False,
192
+ 'drives': []
193
+ }
194
+
195
+ for category, checkboxes in self.checkboxes.items():
196
+ if category == 'drives':
197
+ # drives ist eine Liste von Tupeln: (dev, part, checkbox)
198
+ drive_map = self.hardware_info.get('drive_map', {})
199
+ selected_drives = []
200
+ for entry in self.checkboxes:
201
+ if len(entry) == 3:
202
+ category, checkbox, item = entry
203
+ if category == "drives" and checkbox.GetValue():
204
+ if ":" in item:
205
+ dev, part = item.split(":", 1)
206
+ selected_drives.append((dev.strip(), part.strip()))
207
+
208
+ selected['drives'] = selected_drives
209
+ else:
210
+ # normale checkbox listen
211
+ if any(checkbox.GetValue() for checkbox in checkboxes):
212
+ selected[category] = True
213
+
214
+ # Hardwareinfos ergänzen
215
+ selected['cpu_info'] = self.hardware_info.get('cpu_info', {})
216
+ selected['ram_info'] = self.hardware_info.get('ram_info', {})
217
+ selected['gpu_info'] = self.hardware_info.get('gpu_info', [])
218
+ selected['network_adapters'] = self.hardware_info.get('network_adapters', [])
219
+ selected_components['drive_map'] = self.hardware_info.get('drive_map', {})
220
+
221
+ return selected
222
+
223
+ def on_close(self, event):
224
+ print("[DEBUG] Fenster wird geschlossen")
225
+
226
+ if hasattr(self, 'timer') and self.timer.IsRunning():
227
+ self.timer.Stop()
228
+
229
+ if self.result_queue:
230
+ # Leere Auswahl übermitteln
231
+ selected_components = {
232
+ 'cpu': False,
233
+ 'ram': False,
234
+ 'gpu': False,
235
+ 'network': False,
236
+ 'drives': [],
237
+ 'cpu_info': self.hardware_info.get('cpu_info', {}),
238
+ 'ram_info': self.hardware_info.get('ram_info', {}),
239
+ 'gpu_info': self.hardware_info.get('gpu_info', []),
240
+ 'network_adapters': self.hardware_info.get('network_adapters', []),
241
+ 'drive_map': self.hardware_info.get('drive_map', {})
242
+ }
243
+ self.result_queue.put(selected_components)
244
+
245
+ self.Destroy()
246
+
247
+ app = wx.GetApp()
248
+ if app:
249
+ app.ExitMainLoop()
250
+
251
+
252
+
253
+
254
+
255
+ if __name__ == "__main__":
256
+ app = wx.App(False)
257
+
258
+ # Layout
259
+ hardware_info = {
260
+ 'cpu_info': {
261
+ 'logical_cores': 8,
262
+ 'physical_cores': 4,
263
+ 'frequency': 3200,
264
+ },
265
+ 'ram_info': {
266
+ 'total_gb': 16,
267
+ 'available_gb': 10
268
+ },
269
+ 'gpu_info': [
270
+ {'name': 'NVIDIA GTX 1080', 'memory_total_mb': 8192, 'max_temp': 84}
271
+ ],
272
+ 'network_adapters': ['Ethernet', 'Wi-Fi'],
273
+ 'drive_map': {
274
+ 'Disk0': [{'letter': 'C:', 'label': 'System'}, {'letter': 'D:', 'label': 'Recovery'}],
275
+ 'Disk1': [{'letter': 'E:', 'label': 'Data'}]
276
+ }
277
+
278
+
279
+ }
280
+
281
+ frame = MainFrame(None, hardware_info=hardware_info)
282
+ frame.Show()
283
+ app.MainLoop()
hardware.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # hardware.py
2
+ import psutil
3
+ import wmi
4
+ import pynvml
5
+ import threading
6
+ import time
7
+
8
+
9
+ def safe_call(func, name):
10
+ try:
11
+ return func()
12
+ except Exception as e:
13
+ print(f"[WARN] {name} konnte nicht geladen werden: {e}")
14
+ return None
15
+
16
+
17
+ def get_physical_drives_with_partitions_and_labels():
18
+ c = wmi.WMI()
19
+ drive_map = {}
20
+
21
+ for disk in c.Win32_DiskDrive():
22
+ disk_id = disk.DeviceID.split("\\")[-1].upper()
23
+ if disk_id not in drive_map:
24
+ drive_map[disk_id] = []
25
+
26
+ partitions = disk.associators("Win32_DiskDriveToDiskPartition")
27
+ for partition in partitions:
28
+ logical_disks = partition.associators("Win32_LogicalDiskToPartition")
29
+ for logical_disk in logical_disks:
30
+ letter = logical_disk.DeviceID.upper().strip()
31
+ volume_name = logical_disk.VolumeName or "Kein Name"
32
+ if not any(d["letter"] == letter for d in drive_map[disk_id]):
33
+ drive_map[disk_id].append({
34
+ "letter": letter,
35
+ "label": volume_name
36
+ })
37
+ print("[DEBUG] Drive Info:", drive_map)
38
+ return drive_map
39
+
40
+
41
+
42
+ def get_cpu_info():
43
+ cpu_freq = psutil.cpu_freq()
44
+ cpu_info = {
45
+ "logical_cores": psutil.cpu_count(logical=True),
46
+ "physical_cores": psutil.cpu_count(logical=False),
47
+ "frequency": round(cpu_freq.max) if cpu_freq else None,
48
+ }
49
+ return cpu_info
50
+
51
+
52
+ def get_ram_info():
53
+ mem = psutil.virtual_memory()
54
+ ram_info = {
55
+ "total_gb": round(mem.total / (1024 ** 3)),
56
+ "available_gb": round(mem.available / (1024 ** 3)),
57
+ }
58
+ print("[DEBUG] RAM Info:", ram_info)
59
+ return ram_info
60
+
61
+
62
+
63
+ def get_gpu_info():
64
+ gpu_info = []
65
+ pynvml.nvmlInit()
66
+ try:
67
+ device_count = pynvml.nvmlDeviceGetCount()
68
+ for i in range(device_count):
69
+ handle = pynvml.nvmlDeviceGetHandleByIndex(i)
70
+ name_raw = pynvml.nvmlDeviceGetName(handle)
71
+ name = name_raw.decode() if isinstance(name_raw, bytes) else name_raw
72
+ mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
73
+
74
+ try:
75
+ max_temp = pynvml.nvmlDeviceGetTemperatureThreshold(
76
+ handle,
77
+ pynvml.NVML_TEMPERATURE_THRESHOLD_GPU_MAX
78
+ )
79
+ except Exception:
80
+ max_temp = 90
81
+
82
+ gpu_info.append({
83
+ "name": name,
84
+ "memory_total_mb": int(mem_info.total / 1024**2),
85
+ "max_temp": max_temp
86
+ })
87
+ except Exception as e:
88
+ print(f"[WARN] GPU-Info konnte nicht geladen werden: {e}")
89
+ finally:
90
+ pynvml.nvmlShutdown()
91
+ print("[DEBUG] RAM Info:", gpu_info)
92
+ return gpu_info
93
+
94
+
95
+ def get_network_adapters():
96
+ c = wmi.WMI()
97
+ adapters = []
98
+ for nic in c.Win32_NetworkAdapterConfiguration(IPEnabled=True):
99
+ if hasattr(nic, 'Description'):
100
+ adapters.append(nic.Description)
101
+ print("[DEBUG] RAM Info:", adapters)
102
+ return adapters
103
+
104
+
105
+ def detect_hardware():
106
+ drive_map = safe_call(get_physical_drives_with_partitions_and_labels, "Laufwerke") or {}
107
+ cpu_info = safe_call(get_cpu_info, "CPU") or {}
108
+ gpu_info = safe_call(get_gpu_info, "GPU") or []
109
+ ram_info = safe_call(get_ram_info, "RAM") or {}
110
+ network_adapters = safe_call(get_network_adapters, "Netzwerkadapter") or []
111
+ '''
112
+ device_partitions = [
113
+ (dev, part)
114
+ for dev, parts in drive_map.items()
115
+ for part in parts
116
+ ]
117
+ '''
118
+ return {
119
+ 'cpu_info': cpu_info,
120
+ 'ram_info': ram_info,
121
+ 'gpu_info': gpu_info,
122
+ 'network_adapters': network_adapters,
123
+ 'drive_map': drive_map
124
+ }
125
+
126
+
127
+
128
+ def main():
129
+ print("[INFO] Starte Hardware-Erkennung...\n")
130
+ hardware_info = detect_hardware()
131
+
132
+ # Optional: Ausgabe der erkannten Hardware (kann auskommentiert werden)
133
+ #for key, value in hardware_info.items():
134
+ # print(f"[RESULT] {key}: {value}")
135
+
136
+ print("\n[INFO] Warte 5 Sekunden...")
137
+ time.sleep(5)
138
+ print("[INFO] Hardware Erkennung beendet.")
139
+
140
+ if __name__ == "__main__":
141
+ main()
142
+
main.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import wx
2
+ import sys
3
+ import threading
4
+ import queue
5
+ import time
6
+ import os
7
+ from gui import MainFrame
8
+ from tray import start_tray_monitoring, shutdown_requested
9
+ from hardware import detect_hardware
10
+
11
+ # main.py
12
+
13
+ def start_gui_and_get_selection(hardware_info, result_queue):
14
+ class App(wx.App):
15
+ def OnInit(self):
16
+ self.frame = MainFrame(None, hardware_info=hardware_info, result_queue=result_queue)
17
+ self.frame.Show()
18
+ return True
19
+
20
+ app = App(False)
21
+ app.MainLoop()
22
+
23
+
24
+ def save_exe_dir_to_meipass():
25
+ try:
26
+ # Ermittlung des MEIPASS-Pfads (nur wenn als .exe via PyInstaller gestartet)
27
+ if hasattr(sys, '_MEIPASS'):
28
+ meipass_dir = sys._MEIPASS
29
+ else:
30
+ print("[WARN] Kein MEIPASS gefunden (nicht als EXE gestartet). Überspringe Speichern.")
31
+ return
32
+
33
+ # Pfad zur laufenden .exe
34
+ if getattr(sys, 'frozen', False):
35
+ #exe_dir = os.path.dirname(sys.executable)
36
+ exe_path = sys.executable # <- vollständiger Pfad zur exe inkl. Dateiname
37
+ else:
38
+ #exe_dir = os.path.dirname(os.path.abspath(__file__))
39
+ exe_path = os.path.abspath(__file__) # <- vollständiger Pfad zur .py Datei
40
+
41
+ # Zieldatei im MEIPASS-Verzeichnis
42
+ output_file = os.path.join(meipass_dir, "startdir.txt")
43
+
44
+ with open(output_file, "w", encoding="utf-8") as f:
45
+ #f.write(f"Startverzeichnis: {exe_dir}\n")
46
+ f.write(f"{exe_path}\n")
47
+
48
+ print(f"[INFO] Startverzeichnis und Startdatei gespeichert in MEIPASS: {output_file}")
49
+ except Exception as e:
50
+ print(f"[ERROR] Fehler beim Schreiben der startdir.txt: {e}")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ try:
55
+ time.sleep(0.5)
56
+
57
+ # Schreibe das Startverzeichnis in den MEIPASS-Ordner
58
+ save_exe_dir_to_meipass()
59
+
60
+ print("[DEBUG] Starte hardware.py...")
61
+ hardware_info = detect_hardware()
62
+ print("[DEBUG] hardware.py exit...")
63
+
64
+ result_queue = queue.Queue()
65
+
66
+ # GUI im MainThread starten!
67
+ start_gui_and_get_selection(hardware_info, result_queue)
68
+
69
+ print("[INFO] GUI beendet.")
70
+
71
+ try:
72
+ selected_components = result_queue.get(timeout=11)
73
+ tray_should_start = any([
74
+ selected_components.get('cpu'),
75
+ selected_components.get('ram'),
76
+ selected_components.get('gpu'),
77
+ selected_components.get('network'),
78
+ bool(selected_components.get('drives'))
79
+ ])
80
+ except queue.Empty:
81
+ print("[WARN] Keine Rückgabe durch GUI. Traymonitor wird nicht gestartet.")
82
+ tray_should_start = False
83
+
84
+ if tray_should_start:
85
+ print("[INFO] Auswahl empfangen:", selected_components)
86
+ time.sleep(1)
87
+
88
+ tray_thread = threading.Thread(
89
+ target=start_tray_monitoring,
90
+ args=(hardware_info, selected_components),
91
+ daemon=False
92
+ )
93
+ tray_thread.start()
94
+
95
+ # Haupt-Exit-Überwachung
96
+ while not shutdown_requested.wait(timeout=0.1):
97
+ pass
98
+
99
+ print("[INFO] Tray-Exit wurde erkannt – beende main.py.")
100
+ sys.exit(0)
101
+
102
+
103
+ print("[INFO] Tray-Monitoring gestartet. GUI ist geschlossen.")
104
+ tray_thread.join()
105
+ else:
106
+ print("[INFO] Programm wird beendet, da keine Auswahl getroffen wurde (GUI geschlossen).")
107
+ sys.exit(0) # Sauber beenden, wenn kein Traymonitor starten soll
108
+
109
+ except KeyboardInterrupt:
110
+ print("[INFO] Manuell beendet.")
111
+ sys.exit(0)
112
+ except Exception as e:
113
+ print(f"[ERROR] Unerwarteter Fehler: {e}", file=sys.stderr)
114
+ sys.exit(1)
115
+
tray.py ADDED
@@ -0,0 +1,830 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tray.py
2
+ import threading
3
+ from threading import Event
4
+ from pystray import Icon, MenuItem, Menu
5
+ from PIL import Image, ImageDraw, ImageFont
6
+ #from functools import partial
7
+ import os
8
+ import sys
9
+ import time
10
+ import psutil
11
+ import pythoncom
12
+ import wmi
13
+ #import ctypes
14
+ import pynvml
15
+ from pynvml import *
16
+ # from pynvml import nvmlDeviceGetTemperature, nvmlDeviceGetTemperatureThreshold, NVML_TEMPERATURE_GPU, NVML_TEMPERATURE_THRESHOLD_GPU_MAX, nvmlInit, nvmlShutdown, nvmlDeviceGetHandleByIndex, nvmlDeviceGetUtilizationRates, nvmlDeviceGetMemoryInfo, nvmlDeviceGetCount, nvmlDeviceGetName
17
+ from difflib import get_close_matches
18
+ import subprocess # Importieren Sie subprocess
19
+ import tempfile
20
+ import shutil
21
+
22
+ icons = {}
23
+ current_colors = {}
24
+ last_colors = {}
25
+ stop_events = {}
26
+ color_lock = threading.Lock()
27
+ icon_lock = threading.Lock()
28
+
29
+
30
+ COLOR_MAP = {
31
+ "gray": (160, 160, 160, 255),
32
+ "green": (0, 128, 0, 255),
33
+ "red": (128, 0, 0, 255),
34
+ "yellow": (220, 220, 0, 255)
35
+ }
36
+
37
+
38
+ def managed_thread(target, *args, **kwargs):
39
+ """
40
+ Führt target(*args, **kwargs) in Schleife aus, bis shutdown_event gesetzt ist.
41
+ Ideal für Monitoring-Loops.
42
+ """
43
+ def wrapper():
44
+ while not shutdown_event.is_set():
45
+ target(*args, **kwargs)
46
+ time.sleep(2.1) # oder individuell einstellbar
47
+ t = threading.Thread(target=wrapper, daemon=True)
48
+ thread_refs.append(t)
49
+ t.start()
50
+
51
+ shutdown_event = Event()
52
+ thread_refs = []
53
+ shutdown_requested = threading.Event()
54
+
55
+
56
+ def resource_path(relative_path):
57
+ try:
58
+ base_path = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
59
+ full_path = os.path.join(base_path, relative_path)
60
+ if not os.path.exists(full_path) and hasattr(sys, 'frozen'):
61
+ raise FileNotFoundError # Trigger fallback
62
+ return full_path
63
+ except (AttributeError, FileNotFoundError):
64
+ try:
65
+ import pkgutil
66
+ data = pkgutil.get_data(__name__, relative_path)
67
+ if data:
68
+ temp_dir = tempfile.gettempdir()
69
+ temp_file = os.path.join(temp_dir, os.path.basename(relative_path))
70
+ with open(temp_file, 'wb') as f:
71
+ f.write(data)
72
+ return temp_file
73
+ except Exception as e:
74
+ print(f"[ERROR] Fallback-Resource-Pfad fehlgeschlagen: {e}")
75
+
76
+ # Last resort: use relative path from package root
77
+ pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "your_package_name"))
78
+ fallback_path = os.path.join(pkg_root, relative_path)
79
+ if os.path.exists(fallback_path):
80
+ return fallback_path
81
+
82
+ raise FileNotFoundError(f"Resource '{relative_path}' not found in any path.")
83
+
84
+
85
+ font_path = resource_path("DePixelSchmal.otf")
86
+
87
+ def round_to_nearest_five(value):
88
+ return int(round(value / 5.0) * 5)
89
+
90
+ def get_active_network_adapters():
91
+ c = wmi.WMI()
92
+ adapters = []
93
+ for nic in c.Win32_NetworkAdapterConfiguration(IPEnabled=True):
94
+ if hasattr(nic, 'Description'):
95
+ adapters.append(nic.Description)
96
+ return adapters
97
+
98
+ def get_adapter_speeds():
99
+ c = wmi.WMI()
100
+ speeds = {}
101
+ for nic in c.Win32_NetworkAdapter():
102
+ if nic.NetEnabled and nic.Speed:
103
+ speeds[nic.Name] = int(nic.Speed) # Bits per second
104
+ return speeds
105
+
106
+ def find_best_match(name, candidates):
107
+ matches = get_close_matches(name, candidates, n=1, cutoff=0.6)
108
+ return matches[0] if matches else None
109
+
110
+ def create_text_icon(text, color=(255, 255, 255, 255), bg_color=(0, 0, 0, 0)):
111
+ size = 77
112
+ image = Image.new("RGBA", (size, size), bg_color)
113
+ draw = ImageDraw.Draw(image)
114
+
115
+ try:
116
+ font = ImageFont.truetype(font_path, 29)
117
+ except Exception:
118
+ font = ImageFont.load_default()
119
+
120
+ bbox = draw.textbbox((0, 0), text, font=font) if hasattr(draw, 'textbbox') else font.getsize(text)
121
+ text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
122
+ text_x = (size - text_w) // 2 - bbox[0]
123
+ text_y = (size - text_h) // 2 - bbox[1]
124
+
125
+ draw.text((text_x, text_y), text, font=font, fill=color)
126
+ return image
127
+
128
+ def format_speed_custom(value_kb):
129
+ units = ['kB/s', 'MB/s', 'GB/s']
130
+ speed = value_kb
131
+ unit_index = 0
132
+
133
+ while speed >= 100 and unit_index < len(units) - 1:
134
+ speed /= 1024
135
+ unit_index += 1
136
+
137
+ if speed < 10:
138
+ display = f"{speed:.1f}"
139
+ else:
140
+ display = f"{min(round(speed), 99)}"
141
+
142
+ return f"{display}\n{units[unit_index]}"
143
+
144
+ # drive icon
145
+ def get_color(read_active, write_active, read_mb=0, write_mb=0):
146
+ if read_mb < 2 and write_mb < 2:
147
+ return "gray"
148
+ elif read_mb >= 2 and write_mb >= 2:
149
+ ratio = read_mb / write_mb if write_mb != 0 else float('inf')
150
+ if 1/5 <= ratio <= 5:
151
+ return "yellow"
152
+ elif write_mb > read_mb:
153
+ return "red"
154
+ else:
155
+ return "green"
156
+ elif write_mb >= 2:
157
+ return "red"
158
+ elif read_mb >= 2:
159
+ return "green"
160
+ return "gray"
161
+
162
+
163
+ def _set_icon_color(key, color):
164
+ with icon_lock:
165
+ icon_data = icons.get(key)
166
+ if icon_data:
167
+ icon = icon_data["icon"]
168
+ label = icon_data["label"]
169
+ new_icon = create_icon(COLOR_MAP.get(color, (128, 128, 128)), label)
170
+ try:
171
+ icon.icon = new_icon
172
+ except Exception as e:
173
+ print(f"[WARN] Could not update icon for {key}: {e}")
174
+ finally:
175
+ with color_lock:
176
+ last_colors[key] = color
177
+
178
+ # Drive letter ICONS
179
+ def create_icon(color_rgb, label):
180
+ size = 77
181
+ image = Image.new("RGBA", (size, size), (0, 0, 0, 0))
182
+ draw = ImageDraw.Draw(image)
183
+
184
+ draw.ellipse((0, 0, size, size), fill=color_rgb)
185
+
186
+ try:
187
+ font = ImageFont.truetype(font_path, 55)
188
+ except Exception:
189
+ font = ImageFont.load_default()
190
+
191
+ if hasattr(draw, 'textbbox'):
192
+ bbox = draw.textbbox((0, 0), label, font=font)
193
+ else:
194
+ width, height = font.getsize(label)
195
+ bbox = (0, 0, width, height)
196
+
197
+ text_w = bbox[2] - bbox[0]
198
+ text_h = bbox[3] - bbox[1]
199
+ text_x = (size - text_w) / 2
200
+ text_y = (size - text_h) / 2 - bbox[1]
201
+
202
+ brightness = sum(color_rgb) / 3
203
+ text_color = (0, 0, 0, 255) if brightness > 130 else (255, 255, 255, 255)
204
+
205
+ draw.text((text_x, text_y), label, font=font, fill=text_color)
206
+ return image
207
+
208
+ def update_tray_color(key, color):
209
+ with color_lock:
210
+ old_color = current_colors.get(key)
211
+ if old_color == color:
212
+ return
213
+ current_colors[key] = color
214
+
215
+
216
+
217
+ def _icon_updater(key, stop_event):
218
+ while not stop_event.is_set():
219
+ with color_lock:
220
+ color = current_colors.get(key, "gray")
221
+ last_color = last_colors.get(key)
222
+
223
+ if color != last_color:
224
+ try:
225
+ _set_icon_color(key, color)
226
+ except Exception as e:
227
+ print(f"[WARN] Icon update failed for {key}: {e}")
228
+
229
+ stop_event.wait(0.2)
230
+
231
+
232
+
233
+ # gradient color bar ICON: cpu, ram, gpu, vram, temp
234
+ def get_gradient_color(percent):
235
+ """
236
+ Gibt eine Farbe für den gegebenen Prozentwert aus einem Regenbogenverlauf zurück:
237
+ 0% -> grün, 50% -> gelb, 100% -> rot
238
+ """
239
+ if percent <= 50:
240
+ # Grün → Gelb
241
+ ratio = percent / 50.0
242
+ r = int(COLOR_MAP["green"][0] + ratio * (COLOR_MAP["yellow"][0] - COLOR_MAP["green"][0]))
243
+ g = int(COLOR_MAP["green"][1] + ratio * (COLOR_MAP["yellow"][1] - COLOR_MAP["green"][1]))
244
+ b = int(COLOR_MAP["green"][2] + ratio * (COLOR_MAP["yellow"][2] - COLOR_MAP["green"][2]))
245
+ else:
246
+ # Gelb → Rot
247
+ ratio = (percent - 50) / 50.0
248
+ r = int(COLOR_MAP["yellow"][0] + ratio * (COLOR_MAP["red"][0] - COLOR_MAP["yellow"][0]))
249
+ g = int(COLOR_MAP["yellow"][1] + ratio * (COLOR_MAP["red"][1] - COLOR_MAP["yellow"][1]))
250
+ b = int(COLOR_MAP["yellow"][2] + ratio * (COLOR_MAP["red"][2] - COLOR_MAP["yellow"][2]))
251
+
252
+ return (r, g, b, 255)
253
+
254
+
255
+ def create_bar_icon(percent, label, color=None):
256
+ """
257
+ Erstellt ein Balken-Icon mit Regenbogen-Farbverlauf.
258
+ 0% → unten grün, 50% → mitte gelb, 100% → oben rot
259
+ """
260
+ size = 77
261
+ margin = 4
262
+ image = Image.new("RGBA", (size, size), (0, 0, 0, 0))
263
+ draw = ImageDraw.Draw(image)
264
+
265
+ # Balkengröße
266
+ bar_width = size - 2 * margin
267
+ bar_height = int((percent / 100.0) * size)
268
+ bar_x0 = margin
269
+ bar_x1 = size - margin
270
+ bar_y_bottom = size - 1
271
+ bar_y_top = bar_y_bottom - bar_height + 1
272
+
273
+ # Farbverlauf zeichnen
274
+ for i in range(bar_height):
275
+ rel_percent = (i / bar_height) * percent
276
+ line_color = get_gradient_color(rel_percent)
277
+ y = bar_y_bottom - i
278
+ draw.line([(bar_x0, y), (bar_x1, y)], fill=line_color)
279
+
280
+ # Label zeichnen
281
+ try:
282
+ font = ImageFont.truetype(font_path, 30)
283
+ except:
284
+ font = ImageFont.load_default()
285
+
286
+ draw.text((2, 2), label, font=font, fill=(255, 255, 255, 255))
287
+
288
+ return image
289
+
290
+
291
+
292
+ def _on_quit(icon_inst=None, item=None):
293
+ print("[INFO] Beenden eingeleitet...")
294
+
295
+ # 1. Stop-Flag setzen und Threads sauber beenden
296
+ shutdown_event.set()
297
+ for t in thread_refs:
298
+ t.join(timeout=2.1)
299
+ print("[INFO] Alle Threads beendet.")
300
+
301
+ # 2. Tray-Icons stoppen
302
+ stop_all_tray_icons()
303
+ print("[INFO] Alle Trays beendet.")
304
+ # 3. pynvml sauber beenden
305
+ try:
306
+ pynvml.nvmlShutdown()
307
+ except Exception:
308
+ pass
309
+ print("[INFO] Nvidia shut down.")
310
+ time.sleep(0.1)
311
+ shutdown_requested.set()
312
+
313
+
314
+ def _on_restart(icon_inst=None, item=None):
315
+ print("[INFO] Neustart eingeleitet...")
316
+
317
+ # 1. Stop-Flag setzen und Threads sauber beenden
318
+ shutdown_event.set()
319
+ for t in thread_refs:
320
+ t.join(timeout=2.1)
321
+ print("[INFO] Alle Threads beendet.")
322
+
323
+ # 2. Tray-Icons stoppen
324
+ stop_all_tray_icons()
325
+
326
+ # 3. pynvml sauber beenden
327
+ try:
328
+ pynvml.nvmlShutdown()
329
+ except Exception:
330
+ pass
331
+
332
+ time.sleep(0.1)
333
+
334
+ base_dir = os.path.dirname(os.path.abspath(__file__))
335
+ startdir_path = os.path.join(base_dir, "startdir.txt")
336
+ print("[INFO] Start Folder lesen")
337
+ try:
338
+ with open(startdir_path, "r", encoding="utf-8") as f:
339
+ executable_path = f.readline().strip()
340
+ if not os.path.isfile(executable_path):
341
+ raise FileNotFoundError(f"EXE nicht gefunden: {executable_path}")
342
+ except Exception as e:
343
+ print(f"[ERROR] Fehler beim Lesen der startdir.txt: {e}")
344
+ if icon_inst:
345
+ icon_inst.stop()
346
+ return
347
+
348
+ exe_dir = os.path.dirname(executable_path)
349
+
350
+ time.sleep(0.1)
351
+ try:
352
+ print(f"[INFO] Starte EXE erneut (via subprocess.Popen): {executable_path}")
353
+
354
+ env = os.environ.copy()
355
+ env["RESTART_COUNT"] = str(int(env.get("RESTART_COUNT", "0")) + 1)
356
+ env["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
357
+
358
+ subprocess.Popen(
359
+ [executable_path],
360
+ cwd=exe_dir,
361
+ env=env,
362
+ close_fds=True,
363
+ shell=False
364
+ )
365
+
366
+ print(f"[INFO] EXE gestartet!")
367
+ except Exception as e:
368
+ print(f"[ERROR] Fehler beim Start via Popen: {e}")
369
+ return
370
+ print("[INFO] Old Instance EXIT")
371
+ time.sleep(0.5)
372
+ shutdown_requested.set()
373
+
374
+
375
+
376
+
377
+ def stop_all_tray_icons():
378
+ for icon in icons.values():
379
+ try:
380
+ if icon["icon"].visible:
381
+ icon["icon"].visible = False
382
+ icon["icon"].stop()
383
+ except Exception as e:
384
+ print(f"[WARN] Icon-Stop fehlgeschlagen: {e}")
385
+ for event in stop_events.values():
386
+ event.set()
387
+
388
+ def update_tray_tooltip(key, tooltip_text):
389
+ icon_data = icons.get(key)
390
+ if icon_data:
391
+ icon = icon_data["icon"]
392
+ try:
393
+ icon.title = tooltip_text[:127]
394
+ except Exception as e:
395
+ print(f"[WARN] Tooltip konnte nicht gesetzt werden für {key}: {e}")
396
+
397
+ def update_net_icons(adapter_name, send_kb, recv_kb, selected_components):
398
+ menu = Menu(
399
+ MenuItem("Restart", lambda icon_inst, item: _on_restart(icon_inst, item)),
400
+ MenuItem("Exit", lambda icon_inst, item: _on_quit(icon_inst, item))
401
+ )
402
+
403
+
404
+ def update_icon(direction, value_kb):
405
+ if not selected_components['network']:
406
+ return
407
+
408
+ if value_kb < 10:
409
+ value_kb = 0
410
+
411
+ key = f"NET_{adapter_name}, Direction: {direction}"
412
+ text_with_linebreak = f"{'U ' if direction == 'SEND' else 'D '}{format_speed_custom(value_kb)}"
413
+ speed_parts = format_speed_custom(value_kb).split("\n")
414
+ text_no_linebreak = f"{speed_parts[0]} {speed_parts[1]}" if len(speed_parts) == 2 else format_speed_custom(value_kb)
415
+
416
+ image = create_text_icon(text_with_linebreak)
417
+ if key not in icons:
418
+ icon = Icon(key, image, menu=menu)
419
+ icons[key] = {"icon": icon, "label": key}
420
+ print(f"Created Network icon {key}")
421
+ icon.run_detached()
422
+ else:
423
+ icons[key]["icon"].icon = image
424
+ # Set tooltip using the update_tray_tooltip function
425
+ tooltip = f"{adapter_name} {'Upload' if direction == 'SEND' else 'Download'}: {text_no_linebreak}"
426
+ tooltip = tooltip[:127] # falls percpu=True (tooltips max128 zeichen)
427
+ update_tray_tooltip(key, tooltip)
428
+
429
+ threading.Thread(target=update_icon, args=("SEND", send_kb), daemon=True).start()
430
+ threading.Thread(target=update_icon, args=("RECV", recv_kb), daemon=True).start()
431
+
432
+
433
+ def sort_selected_drives(drive_selections, device_map):
434
+ items = [(dev, part) for dev, parts in device_map.items() for part in parts]
435
+
436
+ # Zugriff auf 'letter' für Sortierung
437
+ sorted_items = sorted(items, key=lambda x: x[1]['letter'].upper(), reverse=True)
438
+
439
+ # Auch drive_selections sortieren anhand des zweiten Elements (Laufwerksbuchstabe)
440
+ return sorted(drive_selections, key=lambda x: x[1].upper(), reverse=True)
441
+
442
+
443
+ def start_drive_icons(hardware_info, stop_all_tray_icons, device_map, drive_selections):
444
+ print("Starting tray icons for selected drives...")
445
+ menu = Menu(
446
+ MenuItem("Restart", lambda icon_inst, item: _on_restart(icon_inst, item)),
447
+ MenuItem("Exit", lambda icon_inst, item: _on_quit(icon_inst, item))
448
+ )
449
+
450
+
451
+ sorted_drive_selections = sort_selected_drives(drive_selections, device_map)
452
+
453
+ for index, (dev, part) in enumerate(sorted_drive_selections):
454
+ icon_label = part.strip(":")
455
+ icon_key = f"{dev}_{part}"
456
+ icon_title = f"{index}_SmartTaskTool_{icon_label}"
457
+
458
+ image = create_icon(COLOR_MAP["gray"], icon_label)
459
+ icon = Icon(icon_title, image, menu=menu)
460
+ icons[icon_key] = {"icon": icon, "label": icon_label}
461
+ current_colors[icon_key] = "gray"
462
+ last_colors[icon_key] = None
463
+ stop_event = threading.Event()
464
+ stop_events[icon_key] = stop_event
465
+
466
+ icon.run_detached()
467
+
468
+ threading.Thread(target=_icon_updater, args=(icon_key, stop_event), daemon=True).start()
469
+ time.sleep(0.2)
470
+
471
+ print("Started Icons:")
472
+ for key, value in icons.items():
473
+ print(f" {key}: {value}")
474
+
475
+
476
+ def start_tray_monitoring(hardware_info, selected_components):
477
+ if not isinstance(selected_components, dict):
478
+ raise ValueError("selected_components muss ein Dictionary sein")
479
+
480
+ expected_keys = ['cpu', 'ram', 'gpu', 'network', 'drives']
481
+
482
+ # Check if all expected keys are present and have the correct type
483
+ for key in expected_keys:
484
+ if key not in selected_components:
485
+ raise KeyError(f"selected_components fehlt: '{key}'")
486
+
487
+ value = selected_components[key]
488
+
489
+ if key == 'drives':
490
+ if not isinstance(value, list):
491
+ raise TypeError(f"'{key}' muss eine Liste sein.")
492
+ if not all(isinstance(item, tuple) and len(item) == 2 for item in value):
493
+ raise ValueError(f"Jedes Element in '{key}' muss ein Tuple mit zwei Elementen sein.")
494
+ else:
495
+ if not isinstance(value, bool):
496
+ raise TypeError(f"'{key}' muss ein Boolean sein.")
497
+
498
+ print("Starting tray monitoring...")
499
+
500
+ menu = Menu(
501
+ MenuItem("Restart", lambda icon_inst, item: _on_restart(icon_inst, item)),
502
+ MenuItem("Exit", lambda icon_inst, item: _on_quit(icon_inst, item))
503
+ )
504
+
505
+ def update_cpu(percent): # Dummy-Wert, tatsächliche Auswertung erfolgt kontrolliert
506
+ if not selected_components['cpu']:
507
+ return
508
+
509
+ # CPU-Werte kontrolliert ermitteln
510
+ try:
511
+ logical = psutil.cpu_count(logical=True)
512
+ physical = psutil.cpu_count(logical=False)
513
+ cpu_percentages = psutil.cpu_percent(interval=None, percpu=True)
514
+
515
+ num_cores = logical if logical else physical
516
+ core_usages = cpu_percentages[:num_cores]
517
+
518
+ if not core_usages:
519
+ raise ValueError("Keine CPU-Werte erhalten.")
520
+
521
+ avg_cpu_percent = sum(core_usages) / len(core_usages)
522
+ percent = round_to_nearest_five(avg_cpu_percent)
523
+ except Exception as e:
524
+ print(f"[ERROR] CPU-Auswertung fehlgeschlagen: {e}")
525
+ percent = 0
526
+ core_usages = []
527
+ num_cores = 0
528
+
529
+ # Icon-Update
530
+ image = create_bar_icon(percent, "CPU")
531
+ key = "CPU_USAGE"
532
+ if key not in icons:
533
+ icon = Icon(key, image, menu=menu)
534
+ icons[key] = {"icon": icon, "label": "CPU"}
535
+ print(f"Created CPU icon")
536
+ threading.Thread(target=icon.run, daemon=True).start()
537
+ else:
538
+ icons[key]["icon"].icon = image
539
+
540
+ # Tooltip
541
+ try:
542
+ cores_str = " | ".join([f"{int(p)}%" for p in core_usages])
543
+ tooltip = f"{num_cores} Cores: {cores_str}"
544
+ tooltip = tooltip[:127]
545
+ update_tray_tooltip(key, tooltip)
546
+ except Exception:
547
+ tooltip = f"CPU {percent}%"
548
+
549
+ #icons[key]["icon"].title = tooltip
550
+
551
+
552
+ def update_ram(percent):
553
+ if not selected_components['ram']:
554
+ return
555
+ percent = round_to_nearest_five(percent)
556
+ image = create_bar_icon(percent, "RAM")
557
+ key = "RAM_USAGE"
558
+ if key not in icons:
559
+ icon = Icon(key, image, menu=menu)
560
+ icons[key] = {"icon": icon, "label": "RAM"}
561
+ print(f"Created RAM icon")
562
+ threading.Thread(target=icon.run, daemon=True).start()
563
+ else:
564
+ icons[key]["icon"].icon = image
565
+
566
+ try:
567
+ mem = psutil.virtual_memory()
568
+ used_gb = round(mem.used / (1024 ** 3))
569
+ total_gb = round(mem.total / (1024 ** 3))
570
+ tooltip = f"RAM {used_gb} / {total_gb} GB"
571
+ update_tray_tooltip(key, tooltip)
572
+ except Exception:
573
+ tooltip = f"RAM: {percent}%"
574
+ #icons[key]["icon"].title = tooltip
575
+
576
+
577
+ def update_gpu_vram_temp(idx, util, mem_used, mem_total, temp, max_temp):
578
+ if not selected_components['gpu']:
579
+ return
580
+
581
+ # === Temperatur ===
582
+ key_temp = f"GPU{idx}_TEMP"
583
+ label_temp = f"T{idx}"
584
+ temp_rounded = round(temp)
585
+ # Temperatur in Prozent (dynamisch zu max_temp)
586
+ clamped = max(35, min(temp, max_temp)) # untere Grenze: 35°C
587
+ pct = round((clamped - 35) / (max_temp - 35) * 100)
588
+ image_temp = create_bar_icon(pct, label_temp)
589
+
590
+ if key_temp not in icons:
591
+ try:
592
+ icon = Icon(key_temp, image_temp, menu=menu)
593
+ icons[key_temp] = {"icon": icon, "label": label_temp}
594
+ print(f"Created GPU{idx} temperature icon.")
595
+ threading.Thread(target=icon.run, daemon=True).start()
596
+ except Exception as e:
597
+ print(f"Error creating GPU{idx} temperature icon: {e}")
598
+ else:
599
+ icons[key_temp]["icon"].icon = image_temp
600
+
601
+
602
+ tooltip = f"{label_temp}: {temp_rounded} °C / {max_temp} °C"
603
+ tooltip = tooltip[:127]
604
+ update_tray_tooltip(key_temp, tooltip)
605
+ #icons[key_temp]["icon"].title = f"{label_temp}: {temp_rounded} °C / {max_temp} °C"
606
+ time.sleep(0.01)
607
+
608
+
609
+ # === VRAM-Nutzung ===
610
+ key_vram = f"VRAM{idx}_USAGE"
611
+ label_vram = f"VR{idx}"
612
+ vram_util = round_to_nearest_five(mem_used / mem_total * 100)
613
+ image_vram = create_bar_icon(vram_util, label_vram)
614
+
615
+ if key_vram not in icons:
616
+ try:
617
+ icon = Icon(key_vram, image_vram, menu=menu)
618
+ icons[key_vram] = {"icon": icon, "label": label_vram}
619
+ print(f"Created VRAM{idx} usage icon.")
620
+ threading.Thread(target=icon.run, daemon=True).start()
621
+ except Exception as e:
622
+ print(f"Error creating VRAM{idx} usage icon: {e}")
623
+ else:
624
+ icons[key_vram]["icon"].icon = image_vram
625
+
626
+ used_gb = round(mem_used / (1024**3))
627
+ total_gb = round(mem_total / (1024**3))
628
+ # Set tooltip using the update_tray_tooltip function
629
+ tooltip = f"{label_vram}: {used_gb}/{total_gb} GB"
630
+ tooltip = tooltip[:127]
631
+ update_tray_tooltip(key_vram, tooltip)
632
+
633
+ #icons[key_vram]["icon"].title = f"{label_vram}: {used_gb}/{total_gb} GB"
634
+ time.sleep(0.01)
635
+
636
+
637
+ # === GPU-Nutzung ===
638
+ key_gpu = f"GPU{idx}_USAGE"
639
+ label_gpu = f"GPU{idx}"
640
+ image_gpu = create_bar_icon(util, label_gpu)
641
+
642
+ if key_gpu not in icons:
643
+ try:
644
+ icon = Icon(key_gpu, image_gpu, menu=menu)
645
+ icons[key_gpu] = {"icon": icon, "label": label_gpu}
646
+ print(f"Created GPU{idx} usage icon.")
647
+ threading.Thread(target=icon.run, daemon=True).start()
648
+ except Exception as e:
649
+ print(f"Error creating GPU{idx} usage icon: {e}")
650
+ else:
651
+ icons[key_gpu]["icon"].icon = image_gpu
652
+ #icons[key_gpu]["icon"].title = f"{label_gpu}: {util}%"
653
+
654
+ # Set tooltip using the update_tray_tooltip function
655
+ tooltip = f"{label_gpu}: {util}%"
656
+ tooltip = tooltip[:127]
657
+ update_tray_tooltip(key_gpu, tooltip)
658
+
659
+
660
+ def gpu_monitor():
661
+ try:
662
+ pynvml.nvmlInit()
663
+ gpu_data = []
664
+
665
+ for idx in range(pynvml.nvmlDeviceGetCount()):
666
+ handle = pynvml.nvmlDeviceGetHandleByIndex(idx)
667
+ try:
668
+ max_temp = pynvml.nvmlDeviceGetTemperatureThreshold(
669
+ handle, pynvml.NVML_TEMPERATURE_THRESHOLD_GPU_MAX
670
+ )
671
+ except pynvml.NVMLError:
672
+ max_temp = 90 # Fallback
673
+
674
+ gpu_data.append((idx, handle, max_temp))
675
+
676
+ while not shutdown_event.is_set():
677
+ #while True:
678
+ for idx, handle, max_temp in gpu_data:
679
+ try:
680
+ util = pynvml.nvmlDeviceGetUtilizationRates(handle).gpu
681
+ mem = pynvml.nvmlDeviceGetMemoryInfo(handle)
682
+ temp = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
683
+
684
+ update_gpu_vram_temp(idx, util, mem.used, mem.total, temp, max_temp)
685
+
686
+ except pynvml.NVMLError as e:
687
+ print(f"[ERROR] GPU{idx} Fehler: {e}")
688
+
689
+ time.sleep(0.5)
690
+
691
+ except pynvml.NVMLError as e:
692
+ print(f"[ERROR] NVML Initialisierung fehlgeschlagen: {e}")
693
+
694
+
695
+
696
+ def cpu_monitor():
697
+ while not shutdown_event.is_set():
698
+ try:
699
+ update_cpu(psutil.cpu_percent(interval=None))
700
+ except Exception as e:
701
+ print(f"[ERROR] CPU Monitoring Fehler: {e}")
702
+ time.sleep(0.5)
703
+
704
+ def ram_monitor():
705
+ while not shutdown_event.is_set():
706
+ try:
707
+ mem = psutil.virtual_memory()
708
+ update_ram(mem.percent)
709
+ except Exception as e:
710
+ print(f"[ERROR] RAM Monitoring Fehler: {e}")
711
+ time.sleep(0.5)
712
+
713
+ def wmi_monitor(poll_interval=2):
714
+ # if polinterval change -> set MB/s and kb/s
715
+ pythoncom.CoInitialize()
716
+ c = wmi.WMI(namespace="root\\CIMV2")
717
+ prev_disk_counters = {}
718
+ prev_net_stats = {}
719
+ speeds = get_adapter_speeds()
720
+
721
+ active_adapters = get_active_network_adapters()
722
+ adapter_map = {active: find_best_match(active, list(speeds.keys())) for active in active_adapters}
723
+
724
+ try:
725
+ next_time = time.perf_counter()
726
+ while not shutdown_event.is_set():
727
+ #while True:
728
+ try:
729
+
730
+ # Drives
731
+ perf_logical_disks = {
732
+ disk.Name.upper(): disk
733
+ for disk in c.Win32_PerfRawData_PerfDisk_LogicalDisk()
734
+ }
735
+
736
+ for dev, part in selected_components.get('drives', []):
737
+ if not selected_components['drives']:
738
+ continue
739
+
740
+ perf_disk = perf_logical_disks.get(part)
741
+ if not perf_disk:
742
+ continue
743
+
744
+ read_bytes = int(perf_disk.DiskReadBytesPerSec)
745
+ write_bytes = int(perf_disk.DiskWriteBytesPerSec)
746
+ prev_read, prev_write = prev_disk_counters.get(part, (0, 0))
747
+ read_diff = max(0, read_bytes - prev_read)
748
+ write_diff = max(0, write_bytes - prev_write)
749
+ prev_disk_counters[part] = (read_bytes, write_bytes)
750
+
751
+ mb_read = read_diff / 1024 / 1024 / 2
752
+ mb_write = write_diff / 1024 / 1024 / 2
753
+
754
+ key = f"{dev}_{part}"
755
+ color = get_color(mb_read > 2.0, mb_write > 2.0, mb_read, mb_write)
756
+
757
+ update_tray_color(key, color)
758
+ update_tray_tooltip(key, f"{part} R {int(mb_read)} MB/s | W {int(mb_write)} MB/s")
759
+
760
+ # Network
761
+
762
+ perf_net_ifaces = c.Win32_PerfRawData_Tcpip_NetworkInterface()
763
+
764
+ for iface in perf_net_ifaces:
765
+ iface_name = getattr(iface, 'Name', None)
766
+ if not iface_name:
767
+ continue
768
+
769
+ adapter_name = find_best_match(iface_name, list(adapter_map.keys()))
770
+ if not adapter_name or not selected_components['network']:
771
+ continue
772
+
773
+ send_raw = int(iface.BytesSentPersec)
774
+ recv_raw = int(iface.BytesReceivedPerSec)
775
+
776
+ #print(f"Raw Network Data - Send: {send_raw}, Recv: {recv_raw}")
777
+
778
+ prev_send, prev_recv = prev_net_stats.get(adapter_name, (0, 0))
779
+ send_diff = max(0, send_raw - prev_send)
780
+ recv_diff = max(0, recv_raw - prev_recv)
781
+
782
+ #print(f"Network Diff Data - Send: {send_diff}, Recv: {recv_diff}")
783
+
784
+ prev_net_stats[adapter_name] = (send_raw, recv_raw)
785
+
786
+ # Convert bytes to KB
787
+ send_kb = send_diff / 1024 / 2
788
+ recv_kb = recv_diff / 1024 / 2
789
+
790
+ #print(f"Converted Network Data - Send: {send_kb} KB/s, Recv: {recv_kb} KB/s")
791
+
792
+ update_net_icons(adapter_name, send_kb, recv_kb, selected_components)
793
+
794
+ except Exception as e:
795
+ print(f"[ERROR] Unified WMI Monitor: {e}")
796
+
797
+ next_time += poll_interval
798
+ time.sleep(max(0, next_time - time.perf_counter()))
799
+
800
+ finally:
801
+ pythoncom.CoUninitialize()
802
+
803
+
804
+
805
+ if selected_components['cpu']:
806
+ managed_thread(cpu_monitor)
807
+ time.sleep(0.3)
808
+
809
+ if selected_components['ram']:
810
+ managed_thread(ram_monitor)
811
+ time.sleep(0.3)
812
+
813
+ if selected_components['gpu']:
814
+ managed_thread(gpu_monitor)
815
+ time.sleep(0.3)
816
+
817
+ if selected_components['network']:
818
+ managed_thread(wmi_monitor)
819
+ time.sleep(2)
820
+
821
+ # Start tray icons for drives based on user selection
822
+ if selected_components['drives']:
823
+ device_map = hardware_info.get('drive_map', {})
824
+ drive_selections = selected_components['drives']
825
+ start_drive_icons(hardware_info, stop_all_tray_icons, device_map, drive_selections)
826
+
827
+
828
+ print("All tray monitoring components started.")
829
+
830
+