"""
StabilityTester - Interface Graphique avec PySide6
Application moderne pour surveiller la stabilite de connexion internet
"""
import sys
import os
from datetime import datetime, timedelta
import threading
import time
import json
# ============================================================================
# CONFIGURATION MATPLOTLIB - Doit être fait AVANT tout autre import de matplotlib
# ============================================================================
# Forcer matplotlib à utiliser PySide6 comme backend Qt
# C'est CRUCIAL pour éviter les erreurs : "Failed to import PyQt5, PySide2"
# Méthode 1: Définir la variable d'environnement (la plus fiable)
os.environ.setdefault('MPLBACKEND', 'QtAgg')
# Méthode 2: Importer matplotlib et définir le backend
import matplotlib
# Essayer les backends compatibles avec PySide6 (Qt6)
# Note: QtAgg est compatible avec Qt5 et Qt6
backends_to_try = ["QtAgg"]
for backend in backends_to_try:
try:
matplotlib.use(backend)
break
except Exception as e:
print(f"Warning: Cannot use matplotlib backend {backend}: {e}")
continue
else:
# Si QtAgg échoue, essayer Qt5Agg
try:
matplotlib.use("Qt5Agg")
print("Using Qt5Agg backend (Qt5)")
except Exception:
# Fallback sans Qt (pour les graphiques non-interactifs)
matplotlib.use("Agg")
print("Using Agg backend (non-interactive)")
# Maintenant importer les modules matplotlib de base
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
# ============================================================================
# IMPORT PYSIDE6 - Doit être fait APRES la configuration de matplotlib
# ============================================================================
try:
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTabWidget, QFrame, QGroupBox,
QRadioButton, QCheckBox, QButtonGroup, QSpinBox, QDoubleSpinBox,
QTextEdit, QTreeWidget, QTreeWidgetItem, QScrollArea, QFileDialog,
QMessageBox, QProgressBar, QSizePolicy, QSplitter, QStyleFactory
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import (
QFont, QColor, QPalette, QPainter, QLinearGradient, QBrush, QPen, QIcon
)
# ========================================================================
# IMPORT MATPLOTLIB QT BACKEND - Doit être fait APRES PySide6
# ========================================================================
# Avec PySide6 (Qt6) le backend est backend_qtagg ; backend_qt5agg sert de repli.
try:
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
except ImportError:
try:
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
except ImportError:
# Fallback pour les systèmes sans Qt
print("Warning: No Qt backend available for matplotlib, using Agg")
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvasQTAgg
PYQt_AVAILABLE = True
except ImportError as e:
print(f"Erreur: PySide6 non disponible: {e}")
print("Installez PySide6 avec: pip install PySide6")
print("Si le problème persiste, essayez: pip install --upgrade PySide6 matplotlib")
PYQt_AVAILABLE = False
sys.exit(1)
# Import de la logique metier
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.network_tester import NetworkTester, TestResult
from monitor.monitor import StabilityMonitor, MonitorConfig, Alert, AlertLevel, AlertHandler
class StabilityScoreBar(QWidget):
"""Barre de progression personnalisée pour le score de stabilité"""
def __init__(self, parent=None):
super().__init__(parent)
self.score = 0
self.level = "Inconnu"
self._setup_ui()
def _setup_ui(self):
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
# Label du score
self.score_label = QLabel("Score: 0/100")
self.score_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold))
layout.addWidget(self.score_label)
# Barre de progression
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(False)
self.progress_bar.setFixedHeight(20)
# Style personnalisé
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f0f0f0;
text-align: center;
}
QProgressBar::chunk {
border-radius: 4px;
margin: 0.5px;
}
""")
layout.addWidget(self.progress_bar, 1)
# Label du niveau
self.level_label = QLabel("Niveau: Inconnu")
self.level_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold))
layout.addWidget(self.level_label)
self.setLayout(layout)
def set_score(self, score, level="Inconnu", color="gray"):
"""Met à jour le score et le niveau"""
self.score = score
self.level = level
self.score_label.setText(f"Score: {score:.1f}/100")
self.level_label.setText(f"Niveau: {level}")
self.progress_bar.setValue(int(score))
# Définir la couleur de la barre
colors = {
"green": "#5cb85c",
"orange": "#f0ad4e",
"red": "#d9534f",
"gray": "#999999"
}
color_value = colors.get(color, "#999999")
# Appliquer le style avec la couleur
style = f"""
QProgressBar::chunk {{
background-color: {color_value};
}}
"""
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f0f0f0;
text-align: center;
}
""" + style)
class DashboardTab(QWidget):
"""Onglet Tableau de bord"""
def __init__(self, monitor, parent=None):
super().__init__(parent)
self.monitor = monitor
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(15)
# Frame des indicateurs principaux
indicators_frame = QFrame()
indicators_layout = QHBoxLayout()
indicators_layout.setSpacing(20)
# Statut de connexion
self.connection_status_label = QLabel("Statut: Inconnu")
self.connection_status_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
indicators_layout.addWidget(self.connection_status_label)
# Temps de disponibilité
self.uptime_label = QLabel("Disponibilité: 0s")
self.uptime_label.setFont(QFont("Segoe UI", 12))
indicators_layout.addWidget(self.uptime_label)
# Temps d'indisponibilité
self.downtime_label = QLabel("Indisponibilité: 0s")
self.downtime_label.setFont(QFont("Segoe UI", 12))
indicators_layout.addWidget(self.downtime_label)
# Pourcentage de disponibilité
self.uptime_pct_label = QLabel("Dispo: 100%")
self.uptime_pct_label.setFont(QFont("Segoe UI", 12))
indicators_layout.addWidget(self.uptime_pct_label)
indicators_frame.setLayout(indicators_layout)
layout.addWidget(indicators_frame)
# Barre de score de stabilité
self.score_bar = StabilityScoreBar()
layout.addWidget(self.score_bar)
# Séparateur
layout.addWidget(QFrame()) # Espace vide
# Métriques principales
metrics_frame = QFrame()
metrics_layout = QHBoxLayout()
metrics_layout.setSpacing(10)
# Latence
latency_group = QGroupBox("Latence")
latency_vbox = QVBoxLayout()
self.latency_label = QLabel("-- ms")
self.latency_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
self.latency_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
latency_vbox.addWidget(self.latency_label)
latency_group.setLayout(latency_vbox)
metrics_layout.addWidget(latency_group, 1)
# Download
download_group = QGroupBox("Download")
download_vbox = QVBoxLayout()
self.download_label = QLabel("-- Mbps")
self.download_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
self.download_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
download_vbox.addWidget(self.download_label)
download_group.setLayout(download_vbox)
metrics_layout.addWidget(download_group, 1)
# Upload
upload_group = QGroupBox("Upload")
upload_vbox = QVBoxLayout()
self.upload_label = QLabel("-- Mbps")
self.upload_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
self.upload_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
upload_vbox.addWidget(self.upload_label)
upload_group.setLayout(upload_vbox)
metrics_layout.addWidget(upload_group, 1)
# Jitter
jitter_group = QGroupBox("Jitter")
jitter_vbox = QVBoxLayout()
self.jitter_label = QLabel("-- ms")
self.jitter_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
self.jitter_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
jitter_vbox.addWidget(self.jitter_label)
jitter_group.setLayout(jitter_vbox)
metrics_layout.addWidget(jitter_group, 1)
metrics_frame.setLayout(metrics_layout)
layout.addWidget(metrics_frame)
self.setLayout(layout)
def update_status(self, status):
"""Met à jour les indicateurs de statut"""
if status.get('connected', False):
self.connection_status_label.setText("Statut: Connecté")
else:
self.connection_status_label.setText("Statut: Déconnecté")
self.uptime_label.setText(f"Disponibilité: {self._format_timedelta(status.get('uptime', timedelta(0)))}")
self.downtime_label.setText(f"Indisponibilité: {self._format_timedelta(status.get('downtime', timedelta(0)))}")
total = status['uptime'].total_seconds() + status['downtime'].total_seconds()
uptime_pct = (status['uptime'].total_seconds() / total * 100) if total > 0 else 100
if uptime_pct > 95:
color = "#5cb85c"
elif uptime_pct > 80:
color = "#f0ad4e"
else:
color = "#d9534f"
self.uptime_pct_label.setText(f"Dispo: {uptime_pct:.1f}%")
def update_metrics(self, result):
"""Met à jour les métriques"""
if result.latency_ms is not None:
self.latency_label.setText(f"{result.latency_ms:.1f} ms")
if result.jitter is not None:
self.jitter_label.setText(f"{result.jitter:.1f} ms")
if result.download_speed is not None:
self.download_label.setText(f"{result.download_speed:.2f} Mbps")
if result.upload_speed is not None:
self.upload_label.setText(f"{result.upload_speed:.2f} Mbps")
def update_score(self, score_data):
"""Met à jour la barre de score"""
score = score_data.get('score', 0)
level = score_data.get('stability_level', 'Inconnu')
color = score_data.get('color', 'gray')
self.score_bar.set_score(score, level, color)
def _format_timedelta(self, td):
ts = int(td.total_seconds())
if ts < 60:
return f"{ts}s"
elif ts < 3600:
return f"{ts//60}m {ts%60}s"
else:
return f"{ts//3600}h {(ts%3600)//60}m"
class TestsTab(QWidget):
"""Onglet Tests"""
def __init__(self, monitor, tester, parent=None):
super().__init__(parent)
self.monitor = monitor
self.tester = tester
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(15)
# Frame de contrôle
control_frame = QFrame()
control_layout = QHBoxLayout()
# Sélection du type de test
test_label = QLabel("Type de test:")
control_layout.addWidget(test_label)
self.test_type_group = QButtonGroup()
test_types = [
("Complet", "comprehensive"),
("Ping", "ping"),
("Connexion", "connection"),
("DNS", "dns"),
("Download", "download"),
("Upload", "upload"),
("Speedtest", "full_speed"),
("Ping tous", "ping_all")
]
for label, value in test_types:
rb = QRadioButton(label)
rb.setProperty("test_value", value)
self.test_type_group.addButton(rb)
control_layout.addWidget(rb)
# Bouton Exécuter
self.run_button = QPushButton("Exécuter")
self.run_button.setStyleSheet("""
QPushButton {
background-color: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #0056b3;
}
""")
self.run_button.clicked.connect(self._run_selected_test)
control_layout.addWidget(self.run_button)
control_frame.setLayout(control_layout)
layout.addWidget(control_frame)
# Zone de résultats
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
self.result_text.setFont(QFont("Courier New", 10))
layout.addWidget(self.result_text)
self.setLayout(layout)
def _run_selected_test(self):
"""Exécute le test sélectionné"""
test_type = self.test_type_group.checkedButton()
if not test_type:
return
test_value = test_type.property("test_value")
self.result_text.clear()
self.result_text.append("Démarrage du test...")
def run():
try:
if test_value == "comprehensive":
results = self.monitor.run_single_test()
elif test_value == "ping":
results = {'ping': self.tester.ping_test()}
elif test_value == "connection":
results = {'connection': self.tester.connection_test()}
elif test_value == "dns":
results = {'dns': self.tester.dns_test()}
elif test_value == "download":
results = {'download': self.tester.download_speed_test(duration=3)}
elif test_value == "upload":
results = {'upload': self.tester.upload_speed_test(data_size=256*1024)}
elif test_value == "full_speed":
results = {'full_speed': self.tester.full_speed_test()}
elif test_value == "ping_all":
results = {f'ping_{i}': r for i, r in enumerate(self.tester.ping_all_servers())}
else:
results = {}
for test_name, result in results.items():
self.result_text.append(f"\n{'-'*40}")
self.result_text.append(f"Test: {result.test_type}")
self.result_text.append(f" Statut: {'OK' if result.success else 'ERROR'}")
self.result_text.append(f" Heure: {result.timestamp.strftime('%H:%M:%S')}")
if result.latency_ms is not None:
self.result_text.append(f" Latence: {result.latency_ms:.2f} ms")
if result.jitter is not None:
self.result_text.append(f" Jitter: {result.jitter:.2f} ms")
if result.download_speed is not None:
self.result_text.append(f" Download: {result.download_speed:.2f} Mbps")
if result.upload_speed is not None:
self.result_text.append(f" Upload: {result.upload_speed:.2f} Mbps")
if result.error_message:
self.result_text.append(f" Erreur: {result.error_message}")
self.result_text.append(f"\n{'='*40}")
self.result_text.append("Test terminé.")
except Exception as e:
self.result_text.append(f"Erreur: {str(e)}")
thread = threading.Thread(target=run, daemon=True)
thread.start()
class HistoryTab(QWidget):
"""Onglet Historique"""
def __init__(self, monitor, parent=None):
super().__init__(parent)
self.monitor = monitor
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Frame de contrôle
control_frame = QFrame()
control_layout = QHBoxLayout()
# Période
control_layout.addWidget(QLabel("Période:"))
self.period_group = QButtonGroup()
periods = [
("Tout", "all"),
("1h", "hour"),
("24h", "day"),
("Semaine", "week")
]
for label, value in periods:
rb = QRadioButton(label)
rb.setProperty("period_value", value)
self.period_group.addButton(rb)
control_layout.addWidget(rb)
# Bouton Rafraîchir
refresh_button = QPushButton("Rafraîchir")
refresh_button.clicked.connect(self._refresh)
control_layout.addWidget(refresh_button)
control_frame.setLayout(control_layout)
layout.addWidget(control_frame)
# Arbre de l'historique
self.tree = QTreeWidget()
self.tree.setColumnCount(6)
self.tree.setHeaderLabels(["Heure", "Type", "Statut", "Latence", "Download", "Upload"])
self.tree.setColumnWidth(0, 150)
self.tree.setColumnWidth(1, 120)
self.tree.setColumnWidth(2, 80)
self.tree.setColumnWidth(3, 100)
self.tree.setColumnWidth(4, 100)
self.tree.setColumnWidth(5, 100)
layout.addWidget(self.tree)
self.setLayout(layout)
# Timer pour rafraîchissement automatique
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh)
self.refresh_timer.start(5000) # 5 secondes
def _refresh(self):
"""Rafraîchit l'historique"""
self.tree.clear()
now = datetime.now()
period = self.period_group.checkedButton()
if period:
period_value = period.property("period_value")
else:
period_value = "all"
for results in self.monitor.get_test_history():
for result in results.values():
if period_value == "hour" and (now - result.timestamp).total_seconds() > 3600:
continue
elif period_value == "day" and (now - result.timestamp).total_seconds() > 86400:
continue
elif period_value == "week" and (now - result.timestamp).total_seconds() > 604800:
continue
item = QTreeWidgetItem()
item.setText(0, result.timestamp.strftime('%H:%M:%S'))
item.setText(1, result.test_type)
item.setText(2, 'OK' if result.success else 'ERROR')
# Couleur selon le statut
if result.success:
item.setTextColor(2, QColor("#5cb85c"))
else:
item.setTextColor(2, QColor("#d9534f"))
# Valeurs
if result.latency_ms is not None:
item.setText(3, f"{result.latency_ms:.0f}ms")
if result.download_speed is not None:
item.setText(4, f"{result.download_speed:.1f}Mbps")
if result.upload_speed is not None:
item.setText(5, f"{result.upload_speed:.1f}Mbps")
self.tree.addTopLevelItem(item)
class AlertsTab(QWidget):
"""Onglet Alertes"""
def __init__(self, monitor, parent=None):
super().__init__(parent)
self.monitor = monitor
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Frame de contrôle
control_frame = QFrame()
control_layout = QHBoxLayout()
control_layout.addWidget(QLabel("Niveau:"))
self.level_group = QButtonGroup()
levels = [
("Toutes", "all"),
("Critique", "critical"),
("Warning", "warning"),
("Info", "info")
]
for label, value in levels:
rb = QRadioButton(label)
rb.setProperty("level_value", value)
self.level_group.addButton(rb)
control_layout.addWidget(rb)
# Boutons
clear_button = QPushButton("Effacer")
clear_button.clicked.connect(self._clear_alerts)
control_layout.addWidget(clear_button)
refresh_button = QPushButton("Rafraîchir")
refresh_button.clicked.connect(self._refresh)
control_layout.addWidget(refresh_button)
control_frame.setLayout(control_layout)
layout.addWidget(control_frame)
# Arbre des alertes
self.tree = QTreeWidget()
self.tree.setColumnCount(5)
self.tree.setHeaderLabels(["Heure", "Niveau", "Message", "Type", "Valeur"])
self.tree.setColumnWidth(0, 150)
self.tree.setColumnWidth(1, 80)
self.tree.setColumnWidth(2, 300)
self.tree.setColumnWidth(3, 120)
self.tree.setColumnWidth(4, 100)
layout.addWidget(self.tree)
self.setLayout(layout)
# Timer pour rafraîchissement automatique
self.refresh_timer = QTimer()
self.refresh_timer.timeout.connect(self._refresh)
self.refresh_timer.start(5000)
def _refresh(self):
"""Rafraîchit les alertes"""
self.tree.clear()
level = self.level_group.checkedButton()
level_value = level.property("level_value") if level else "all"
for alert in self.monitor.alert_handler.get_alert_history():
if level_value != "all" and alert.level.value != level_value:
continue
item = QTreeWidgetItem()
item.setText(0, alert.timestamp.strftime('%H:%M:%S'))
item.setText(1, alert.level.value.upper())
item.setText(2, alert.message[:50])
item.setText(3, alert.test_type)
item.setText(4, f"{alert.value:.2f}" if alert.value else "--")
# Couleur selon le niveau
if alert.level == AlertLevel.CRITICAL:
item.setTextColor(1, QColor("#dc3545"))
elif alert.level == AlertLevel.WARNING:
item.setTextColor(1, QColor("#ffc107"))
else:
item.setTextColor(1, QColor("#17a2b8"))
self.tree.addTopLevelItem(item)
def _clear_alerts(self):
"""Efface l'historique des alertes"""
reply = QMessageBox.question(
self, "Confirmer",
"Voulez-vous vraiment effacer l'historique des alertes?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.monitor.alert_handler.clear_history()
self._refresh()
class SettingsTab(QWidget):
"""Onglet Paramètres"""
def __init__(self, monitor, parent=None):
super().__init__(parent)
self.monitor = monitor
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(15)
# Paramètres généraux
general_group = QGroupBox("Paramètres généraux")
general_layout = QVBoxLayout()
# Intervalle
interval_layout = QHBoxLayout()
interval_layout.addWidget(QLabel("Intervalle (secondes):"))
self.interval_spin = QSpinBox()
self.interval_spin.setRange(10, 3600)
self.interval_spin.setValue(self.monitor.config.interval)
interval_layout.addWidget(self.interval_spin)
general_layout.addLayout(interval_layout)
# Historique max
history_layout = QHBoxLayout()
history_layout.addWidget(QLabel("Historique maximal:"))
self.max_history_spin = QSpinBox()
self.max_history_spin.setRange(10, 1000)
self.max_history_spin.setValue(self.monitor.config.max_history)
history_layout.addWidget(self.max_history_spin)
general_layout.addLayout(history_layout)
general_group.setLayout(general_layout)
layout.addWidget(general_group)
# Seuils
thresholds_group = QGroupBox("Seuils d'alerte")
thresholds_layout = QVBoxLayout()
# Latence
ping_layout = QHBoxLayout()
ping_layout.addWidget(QLabel("Latence (ms):"))
self.ping_threshold_spin = QDoubleSpinBox()
self.ping_threshold_spin.setRange(1, 1000)
self.ping_threshold_spin.setValue(self.monitor.config.ping_threshold)
ping_layout.addWidget(self.ping_threshold_spin)
thresholds_layout.addLayout(ping_layout)
# Jitter
jitter_layout = QHBoxLayout()
jitter_layout.addWidget(QLabel("Jitter (ms):"))
self.jitter_threshold_spin = QDoubleSpinBox()
self.jitter_threshold_spin.setRange(1, 500)
self.jitter_threshold_spin.setValue(self.monitor.config.jitter_threshold)
jitter_layout.addWidget(self.jitter_threshold_spin)
thresholds_layout.addLayout(jitter_layout)
# Download
download_layout = QHBoxLayout()
download_layout.addWidget(QLabel("Download (Mbps):"))
self.download_threshold_spin = QDoubleSpinBox()
self.download_threshold_spin.setRange(0.1, 100)
self.download_threshold_spin.setValue(self.monitor.config.download_threshold)
download_layout.addWidget(self.download_threshold_spin)
thresholds_layout.addLayout(download_layout)
# Upload
upload_layout = QHBoxLayout()
upload_layout.addWidget(QLabel("Upload (Mbps):"))
self.upload_threshold_spin = QDoubleSpinBox()
self.upload_threshold_spin.setRange(0.1, 100)
self.upload_threshold_spin.setValue(self.monitor.config.upload_threshold)
upload_layout.addWidget(self.upload_threshold_spin)
thresholds_layout.addLayout(upload_layout)
thresholds_group.setLayout(thresholds_layout)
layout.addWidget(thresholds_group)
# Notifications
notification_group = QGroupBox("Notifications")
notification_layout = QVBoxLayout()
self.enable_sound_check = QCheckBox("Alertes sonores")
self.enable_sound_check.setChecked(self.monitor.config.enable_sound)
notification_layout.addWidget(self.enable_sound_check)
self.enable_notifications_check = QCheckBox("Notifications système")
self.enable_notifications_check.setChecked(self.monitor.config.enable_notifications)
notification_layout.addWidget(self.enable_notifications_check)
notification_group.setLayout(notification_layout)
layout.addWidget(notification_group)
# Types de tests
tests_group = QGroupBox("Types de tests à exécuter")
tests_layout = QVBoxLayout()
self.test_checks = {}
test_types = [
("Ping", "ping"),
("Connexion HTTP", "connection"),
("Résolution DNS", "dns"),
("Test Download", "download"),
("Test Upload", "upload"),
("Speedtest complet", "full_speed")
]
for label, key in test_types:
cb = QCheckBox(label)
cb.setChecked(key in self.monitor.config.test_types)
self.test_checks[key] = cb
tests_layout.addWidget(cb)
tests_group.setLayout(tests_layout)
layout.addWidget(tests_group)
# Boutons
button_layout = QHBoxLayout()
apply_button = QPushButton("Appliquer")
apply_button.clicked.connect(self._apply_settings)
button_layout.addWidget(apply_button)
reset_button = QPushButton("Réinitialiser")
reset_button.clicked.connect(self._reset_settings)
button_layout.addWidget(reset_button)
layout.addLayout(button_layout)
self.setLayout(layout)
def _apply_settings(self):
"""Applique les paramètres"""
try:
test_types = [
key for key, cb in self.test_checks.items()
if cb.isChecked()
]
new_config = MonitorConfig(
interval=self.interval_spin.value(),
ping_threshold=self.ping_threshold_spin.value(),
download_threshold=self.download_threshold_spin.value(),
upload_threshold=self.upload_threshold_spin.value(),
jitter_threshold=self.jitter_threshold_spin.value(),
enable_sound=self.enable_sound_check.isChecked(),
enable_notifications=self.enable_notifications_check.isChecked(),
max_history=self.max_history_spin.value(),
test_types=test_types
)
self.monitor.config = new_config
self.monitor.alert_handler = AlertHandler(new_config)
QMessageBox.information(self, "Succès", "Paramètres appliqués!")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur: {str(e)}")
def _reset_settings(self):
"""Réinitialise les paramètres"""
reply = QMessageBox.question(
self, "Confirmer",
"Voulez-vous vraiment réinitialiser les paramètres?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.interval_spin.setValue(60)
self.max_history_spin.setValue(100)
self.ping_threshold_spin.setValue(100.0)
self.jitter_threshold_spin.setValue(50.0)
self.download_threshold_spin.setValue(5.0)
self.upload_threshold_spin.setValue(2.0)
self.enable_sound_check.setChecked(True)
self.enable_notifications_check.setChecked(True)
for cb in self.test_checks.values():
cb.setChecked(True)
class StatisticsTab(QWidget):
"""Onglet Statistiques avec graphiques"""
def __init__(self, monitor, parent=None):
super().__init__(parent)
self.monitor = monitor
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(15)
# Statistiques globales
stats_frame = QFrame()
stats_layout = QHBoxLayout()
stats_layout.setSpacing(20)
self.total_tests_label = QLabel("Tests: 0")
self.total_tests_label.setFont(QFont("Segoe UI", 12))
stats_layout.addWidget(self.total_tests_label)
self.success_rate_label = QLabel("Succès: 0%")
self.success_rate_label.setFont(QFont("Segoe UI", 12))
stats_layout.addWidget(self.success_rate_label)
self.connection_loss_label = QLabel("Pertes: 0")
self.connection_loss_label.setFont(QFont("Segoe UI", 12))
stats_layout.addWidget(self.connection_loss_label)
self.avg_latency_label = QLabel("Latence avg: 0ms")
self.avg_latency_label.setFont(QFont("Segoe UI", 12))
stats_layout.addWidget(self.avg_latency_label)
stats_frame.setLayout(stats_layout)
layout.addWidget(stats_frame)
# Onglets des graphiques
self.chart_tabs = QTabWidget()
layout.addWidget(self.chart_tabs)
# Graphique de latence
self._create_latency_chart()
# Graphique de vitesse
self._create_speed_chart()
# Graphique de connectivité
self._create_connectivity_chart()
self.setLayout(layout)
# Timer pour mise à jour
self.update_timer = QTimer()
self.update_timer.timeout.connect(self._update_statistics)
self.update_timer.start(5000)
def _create_latency_chart(self):
"""Crée le graphique de latence"""
chart_widget = QWidget()
chart_layout = QVBoxLayout()
self.latency_fig = Figure(figsize=(8, 4), dpi=100)
self.latency_ax = self.latency_fig.add_subplot(111)
self.latency_ax.set_title("Latence (ms)")
self.latency_ax.set_ylim(0, 500)
self.latency_ax.grid(True, alpha=0.3)
self.latency_canvas = FigureCanvasQTAgg(self.latency_fig)
chart_layout.addWidget(self.latency_canvas)
chart_widget.setLayout(chart_layout)
self.chart_tabs.addTab(chart_widget, "Latence")
self.latency_times = []
self.latency_values = []
def _create_speed_chart(self):
"""Crée le graphique de vitesse"""
chart_widget = QWidget()
chart_layout = QVBoxLayout()
self.speed_fig = Figure(figsize=(8, 4), dpi=100)
self.speed_ax = self.speed_fig.add_subplot(111)
self.speed_ax.set_title("Vitesses (Mbps)")
self.speed_ax.set_ylim(0, 100)
self.speed_ax.grid(True, alpha=0.3)
self.speed_canvas = FigureCanvasQTAgg(self.speed_fig)
chart_layout.addWidget(self.speed_canvas)
chart_widget.setLayout(chart_layout)
self.chart_tabs.addTab(chart_widget, "Vitesse")
self.speed_times = []
self.download_values = []
self.upload_values = []
def _create_connectivity_chart(self):
"""Crée le graphique de connectivité"""
chart_widget = QWidget()
chart_layout = QVBoxLayout()
self.connectivity_fig = Figure(figsize=(8, 4), dpi=100)
self.connectivity_ax = self.connectivity_fig.add_subplot(111)
self.connectivity_ax.set_title("Connectivité")
self.connectivity_ax.set_ylim(-0.5, 1.5)
self.connectivity_ax.set_yticks([0, 1])
self.connectivity_ax.set_yticklabels(['Déconnecté', 'Connecté'])
self.connectivity_ax.grid(True, alpha=0.3)
self.connectivity_canvas = FigureCanvasQTAgg(self.connectivity_fig)
chart_layout.addWidget(self.connectivity_canvas)
chart_widget.setLayout(chart_layout)
self.chart_tabs.addTab(chart_widget, "Connectivité")
self.connectivity_times = []
self.connectivity_values = []
def _update_statistics(self):
"""Met à jour les statistiques"""
try:
stats = self.monitor.get_statistics()
self.total_tests_label.setText(f"Tests: {stats['total_tests']}")
self.success_rate_label.setText(f"Succès: {stats['success_rate']:.1f}%")
self.connection_loss_label.setText(f"Pertes: {stats['connection_loss_count']}")
self.avg_latency_label.setText(f"Latence avg: {stats['avg_latency']:.1f}ms")
# Mettre à jour les graphiques
self._update_latency_chart()
self._update_speed_chart()
self._update_connectivity_chart()
except Exception as e:
print(f"Erreur dans _update_statistics: {e}")
def _update_latency_chart(self):
"""Met à jour le graphique de latence"""
try:
self.latency_times.clear()
self.latency_values.clear()
for i, results in enumerate(self.monitor.get_test_history(limit=20)):
for result in results.values():
if result.latency_ms is not None:
self.latency_times.append(i)
self.latency_values.append(result.latency_ms)
break
if self.latency_times:
self.latency_ax.clear()
self.latency_ax.plot(
self.latency_times,
self.latency_values,
'b-',
linewidth=2
)
self.latency_ax.fill_between(
self.latency_times,
self.latency_values,
alpha=0.3
)
self.latency_ax.axhline(
y=self.monitor.config.ping_threshold,
color='r',
linestyle='--'
)
self.latency_ax.set_title("Latence (ms)")
self.latency_ax.set_ylim(0, max(max(self.latency_values) * 1.2, 100))
self.latency_ax.grid(True, alpha=0.3)
self.latency_canvas.draw()
except Exception as e:
print(f"Erreur dans _update_latency_chart: {e}")
def _update_speed_chart(self):
"""Met à jour le graphique de vitesse"""
try:
self.speed_times.clear()
self.download_values.clear()
self.upload_values.clear()
for i, results in enumerate(self.monitor.get_test_history(limit=20)):
for result in results.values():
if result.download_speed is not None:
self.speed_times.append(i)
self.download_values.append(result.download_speed)
if result.upload_speed is not None:
if len(self.upload_values) <= i:
self.upload_values.append(result.upload_speed)
else:
self.upload_values[i] = result.upload_speed
if self.speed_times:
self.speed_ax.clear()
if self.download_values:
self.speed_ax.plot(
self.speed_times,
self.download_values,
'g-',
label='Download',
linewidth=2
)
if self.upload_values:
self.speed_ax.plot(
self.speed_times,
self.upload_values,
'r-',
label='Upload',
linewidth=2
)
self.speed_ax.axhline(
y=self.monitor.config.download_threshold,
color='g',
linestyle='--',
alpha=0.5
)
self.speed_ax.axhline(
y=self.monitor.config.upload_threshold,
color='r',
linestyle='--',
alpha=0.5
)
self.speed_ax.set_title("Vitesses (Mbps)")
max_speed = max(
max(self.download_values or [0]) * 1.2,
max(self.upload_values or [0]) * 1.2,
10
)
self.speed_ax.set_ylim(0, max_speed)
self.speed_ax.legend()
self.speed_ax.grid(True, alpha=0.3)
self.speed_canvas.draw()
except Exception as e:
print(f"Erreur dans _update_speed_chart: {e}")
def _update_connectivity_chart(self):
"""Met à jour le graphique de connectivité"""
try:
ch = (self.monitor.connection_history[-20:]
if len(self.monitor.connection_history) > 20
else self.monitor.connection_history)
self.connectivity_times = list(range(len(ch)))
self.connectivity_values = [1 if c else 0 for c in ch]
if self.connectivity_times:
self.connectivity_ax.clear()
self.connectivity_ax.step(
self.connectivity_times,
self.connectivity_values,
where='post',
color='blue',
linewidth=2
)
for i in range(len(self.connectivity_times) - 1):
color = 'red' if self.connectivity_values[i] == 0 else 'green'
self.connectivity_ax.axvspan(
self.connectivity_times[i],
self.connectivity_times[i + 1],
facecolor=color,
alpha=0.3
)
self.connectivity_ax.set_title("Connectivité")
self.connectivity_ax.set_ylim(-0.5, 1.5)
self.connectivity_ax.set_yticks([0, 1])
self.connectivity_ax.set_yticklabels(['Déconnecté', 'Connecté'])
self.connectivity_ax.grid(True, alpha=0.3)
self.connectivity_canvas.draw()
except Exception as e:
print(f"Erreur dans _update_connectivity_chart: {e}")
class StabilityTesterWindow(QMainWindow):
"""Fenêtre principale de l'application"""
def __init__(self):
super().__init__()
self.setWindowTitle("StabilityTester")
self.setGeometry(100, 100, 1000, 700)
self.setMinimumSize(800, 600)
# Initialisation
self.monitor = StabilityMonitor()
self.tester = NetworkTester()
self.running = False
# Configuration du style
self._configure_style()
# Création de l'interface
self._create_widgets()
# Connexion des signaux
self.monitor.register_status_callback(self._update_status)
self.monitor.register_alert_callback(self._show_alert)
# Timer pour mise à jour
self.update_timer = QTimer()
self.update_timer.timeout.connect(self._update_ui)
self.update_timer.start(1000) # 1 seconde
# Gestion de la fermeture
self.setWindowIconText("StabilityTester")
def _configure_style(self):
"""Configure le style de l'application"""
# Utiliser le style Fusion pour un look moderne
QApplication.setStyle(QStyleFactory.create("Fusion"))
# Palette moderne
palette = QPalette()
palette.setColor(QPalette.Window, QColor(248, 249, 250))
palette.setColor(QPalette.WindowText, QColor(51, 51, 51))
palette.setColor(QPalette.Base, QColor(255, 255, 255))
palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240))
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0))
palette.setColor(QPalette.Text, QColor(51, 51, 51))
palette.setColor(QPalette.Button, QColor(240, 240, 240))
palette.setColor(QPalette.ButtonText, QColor(51, 51, 51))
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
palette.setColor(QPalette.Link, QColor(0, 100, 180))
palette.setColor(QPalette.Highlight, QColor(0, 120, 200))
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
QApplication.setPalette(palette)
def _create_widgets(self):
"""Crée tous les widgets"""
# Widget central
central_widget = QWidget()
central_layout = QVBoxLayout()
central_layout.setContentsMargins(0, 0, 0, 0)
central_layout.setSpacing(0)
# Barre d'outils
self._create_toolbar(central_layout)
# Onglets
self.tab_widget = QTabWidget()
central_layout.addWidget(self.tab_widget)
# Créer les onglets
self.dashboard_tab = DashboardTab(self.monitor)
self.tab_widget.addTab(self.dashboard_tab, "📋 Tableau de bord")
self.tests_tab = TestsTab(self.monitor, self.tester)
self.tab_widget.addTab(self.tests_tab, "🧪 Tests")
self.history_tab = HistoryTab(self.monitor)
self.tab_widget.addTab(self.history_tab, "📜 Historique")
self.alerts_tab = AlertsTab(self.monitor)
self.tab_widget.addTab(self.alerts_tab, "⚠️ Alertes")
self.settings_tab = SettingsTab(self.monitor)
self.tab_widget.addTab(self.settings_tab, "⚙️ Paramètres")
self.statistics_tab = StatisticsTab(self.monitor)
self.tab_widget.addTab(self.statistics_tab, "📈 Statistiques")
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
# Mettre à jour l'UI
self._update_ui()
def _create_toolbar(self, layout):
"""Crée la barre d'outils"""
toolbar = QWidget()
toolbar_layout = QHBoxLayout()
toolbar_layout.setContentsMargins(15, 10, 15, 10)
toolbar_layout.setSpacing(10)
# Bouton Démarrer
self.start_button = QPushButton("🟢 Démarrer")
self.start_button.setFixedHeight(35)
self.start_button.clicked.connect(self._start_monitoring)
toolbar_layout.addWidget(self.start_button)
# Bouton Arrêter
self.stop_button = QPushButton("🔴 Arrêter")
self.stop_button.setFixedHeight(35)
self.stop_button.setEnabled(False)
self.stop_button.clicked.connect(self._stop_monitoring)
toolbar_layout.addWidget(self.stop_button)
# Bouton Test unique
test_button = QPushButton("🧪 Test unique")
test_button.setFixedHeight(35)
test_button.clicked.connect(self._run_single_test)
toolbar_layout.addWidget(test_button)
# Séparateur
toolbar_layout.addStretch(1)
# Boutons Exporter, Sauvegarder, Charger
export_button = QPushButton("📤 Exporter")
export_button.setFixedHeight(35)
export_button.clicked.connect(self._export_data)
toolbar_layout.addWidget(export_button)
save_button = QPushButton("💾 Sauvegarder")
save_button.setFixedHeight(35)
save_button.clicked.connect(self._save_state)
toolbar_layout.addWidget(save_button)
load_button = QPushButton("📂 Charger")
load_button.setFixedHeight(35)
load_button.clicked.connect(self._load_state)
toolbar_layout.addWidget(load_button)
# Séparateur
toolbar_layout.addStretch(1)
# Bouton Minimiser
minimize_button = QPushButton("📉 Minimiser")
minimize_button.setFixedHeight(35)
minimize_button.clicked.connect(self.showMinimized)
toolbar_layout.addWidget(minimize_button)
# Bouton Quitter
quit_button = QPushButton("❌ Quitter")
quit_button.setFixedHeight(35)
quit_button.clicked.connect(self.close)
toolbar_layout.addWidget(quit_button)
# Indicateurs d'état
toolbar_layout.addStretch(1)
self.connection_indicator = QLabel("● Déconnecté")
self.connection_indicator.setStyleSheet("color: #d9534f; font-weight: bold;")
toolbar_layout.addWidget(self.connection_indicator)
self.monitor_indicator = QLabel("● Surveillance: Inactive")
self.monitor_indicator.setStyleSheet("color: #999; font-weight: bold;")
toolbar_layout.addWidget(self.monitor_indicator)
toolbar.setLayout(toolbar_layout)
layout.addWidget(toolbar)
def _start_monitoring(self):
"""Démarre la surveillance"""
if not self.running:
self.running = True
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.monitor_indicator.setText("● Surveillance: Active")
self.monitor_indicator.setStyleSheet("color: #5cb85c; font-weight: bold;")
self.monitor.start()
def _stop_monitoring(self):
"""Arrête la surveillance"""
if self.running:
self.running = False
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
self.monitor_indicator.setText("● Surveillance: Inactive")
self.monitor_indicator.setStyleSheet("color: #999; font-weight: bold;")
self.monitor.stop()
def _run_single_test(self):
"""Effectue un test unique"""
def run():
try:
results = self.monitor.run_single_test()
for test_name, result in results.items():
self.dashboard_tab.update_metrics(result)
if result.test_type in ["connection", "ping"]:
if result.success:
self.connection_indicator.setText("● Connecté")
self.connection_indicator.setStyleSheet("color: #5cb85c; font-weight: bold;")
else:
self.connection_indicator.setText("● Déconnecté")
self.connection_indicator.setStyleSheet("color: #d9534f; font-weight: bold;")
except Exception as e:
print(f"Erreur lors du test: {e}")
thread = threading.Thread(target=run, daemon=True)
thread.start()
def _update_status(self, status):
"""Met à jour l'état"""
self.dashboard_tab.update_status(status)
if status.get('connected', False):
self.connection_indicator.setText("● Connecté")
self.connection_indicator.setStyleSheet("color: #5cb85c; font-weight: bold;")
else:
self.connection_indicator.setText("● Déconnecté")
self.connection_indicator.setStyleSheet("color: #d9534f; font-weight: bold;")
def _show_alert(self, alert):
"""Affiche une alerte"""
print(f"[ALERTE {alert.level.value.upper()}] {alert.message}")
# Clignoter l'onglet Alertes
if self.tab_widget.currentIndex() != 3:
self._blink_tab(3)
def _blink_tab(self, tab_index):
"""Fait clignoter un onglet"""
original_text = self.tab_widget.tabText(tab_index)
for _ in range(3):
self.tab_widget.setTabText(tab_index, f"⚠️ {original_text}")
QApplication.processEvents()
time.sleep(0.3)
self.tab_widget.setTabText(tab_index, original_text)
QApplication.processEvents()
time.sleep(0.3)
def _update_ui(self):
"""Met à jour l'interface"""
try:
# Mettre à jour le score de stabilité
score_data = self.monitor.get_stability_score()
self.dashboard_tab.update_score(score_data)
# Mettre à jour les statistiques
self.statistics_tab._update_statistics()
# Mettre à jour l'historique
self.history_tab._refresh()
# Mettre à jour les alertes
self.alerts_tab._refresh()
except Exception as e:
print(f"Erreur dans _update_ui: {e}")
def _export_data(self):
"""Exporte les données"""
filepath, _ = QFileDialog.getSaveFileName(
self, "Exporter les données",
"", "JSON Files (*.json);;All Files (*)"
)
if filepath:
try:
data = {
'config': self.monitor.config.to_dict(),
'status': self.monitor.get_current_status(),
'stats': self.monitor.get_statistics(),
'history': [
{n: r.to_dict() for n, r in res.items()}
for res in self.monitor.get_test_history()
],
'alerts': [
a.to_dict() for a in self.monitor.alert_handler.get_alert_history()
],
'exported_at': datetime.now().isoformat()
}
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
QMessageBox.information(self, "Succès", f"Données exportées vers {filepath}")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export: {str(e)}")
def _save_state(self):
"""Sauvegarde l'état"""
try:
self.monitor.save_state()
QMessageBox.information(self, "Succès", "État sauvegardé!")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde: {str(e)}")
def _load_state(self):
"""Charge l'état"""
try:
if self.monitor.load_state():
QMessageBox.information(self, "Succès", "État chargé!")
self._update_ui()
# Mettre à jour les paramètres de l'interface
self.settings_tab.interval_spin.setValue(self.monitor.config.interval)
self.settings_tab.max_history_spin.setValue(self.monitor.config.max_history)
self.settings_tab.ping_threshold_spin.setValue(self.monitor.config.ping_threshold)
self.settings_tab.jitter_threshold_spin.setValue(self.monitor.config.jitter_threshold)
self.settings_tab.download_threshold_spin.setValue(self.monitor.config.download_threshold)
self.settings_tab.upload_threshold_spin.setValue(self.monitor.config.upload_threshold)
self.settings_tab.enable_sound_check.setChecked(self.monitor.config.enable_sound)
self.settings_tab.enable_notifications_check.setChecked(self.monitor.config.enable_notifications)
for key, cb in self.settings_tab.test_checks.items():
cb.setChecked(key in self.monitor.config.test_types)
else:
QMessageBox.information(self, "Info", "Aucun état trouvé, valeurs par défaut")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur lors du chargement: {str(e)}")
def closeEvent(self, event):
"""Gère la fermeture de la fenêtre"""
reply = QMessageBox.question(
self, "Quitter",
"Voulez-vous vraiment quitter StabilityTester?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self._stop_monitoring()
event.accept()
else:
event.ignore()
def run_application():
"""Lance l'application PySide6"""
app = QApplication(sys.argv)
app.setApplicationName("StabilityTester")
app.setApplicationVersion("1.0.1")
window = StabilityTesterWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
run_application()