initial commit

This commit is contained in:
2026-03-24 07:17:48 +01:00
commit 371cbce202
147 changed files with 18244 additions and 0 deletions

687
app.py Normal file
View File

@@ -0,0 +1,687 @@
"""
PC Monitor - Serveur Flask pour monitoring hardware
Supporte plusieurs plugins: LibreHardwareMonitor, HWiNFO, Plexamp, etc.
"""
from flask import Flask, render_template, jsonify, request
import json
import os
import subprocess
from threading import Thread
import time
# Import du système de plugins
from plugins import get_plugin, get_available_plugins, PLUGINS
from plugins import get_media_plugin, get_available_media_plugins, MEDIA_PLUGINS
app = Flask(__name__)
app.config['SECRET_KEY'] = 'votre-cle-secrete-ici'
# Fichiers de configuration
CONFIG_FILE = 'config.json'
# Plugins actifs (initialisés au démarrage)
active_plugin = None
plexamp_plugin = None
# Configuration par défaut
DEFAULT_CONFIG = {
"active_plugin": "librehardwaremonitor",
"theme": "dark",
"apps": [],
"apps_tab": "",
"plugins": {
"librehardwaremonitor": {
"host": "127.0.0.1",
"port": 8085
},
"hwinfo": {
"host": "127.0.0.1",
"port": 60000
}
},
"media": {
"plexamp": {
"enabled": False,
"host": "127.0.0.1",
"port": 32400,
"token": ""
}
}
}
DEFAULT_PLUGIN_CONFIG = {
"tabs": [
{
"id": "tab1",
"name": "Général",
"sensors": []
}
]
}
# ==================== Gestion de la configuration ====================
def load_config():
"""Charge la configuration globale"""
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
# Fusionner avec les valeurs par défaut
for key, value in DEFAULT_CONFIG.items():
if key not in config:
config[key] = value
return config
except Exception as e:
print(f"Erreur chargement config: {e}")
return DEFAULT_CONFIG.copy()
return DEFAULT_CONFIG.copy()
def save_config(config):
"""Sauvegarde la configuration globale"""
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def get_plugin_config_file(plugin_id):
"""Retourne le nom du fichier de config pour un plugin"""
return f'config_{plugin_id}.json'
def load_plugin_config(plugin_id):
"""Charge la configuration spécifique à un plugin"""
config_file = get_plugin_config_file(plugin_id)
if os.path.exists(config_file):
try:
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Erreur chargement config plugin {plugin_id}: {e}")
return DEFAULT_PLUGIN_CONFIG.copy()
return DEFAULT_PLUGIN_CONFIG.copy()
def save_plugin_config(plugin_id, config):
"""Sauvegarde la configuration spécifique à un plugin"""
config_file = get_plugin_config_file(plugin_id)
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def get_merged_config():
"""
Retourne une config fusionnée (global + plugin actif)
Pour compatibilité avec le code existant
"""
global_config = load_config()
plugin_id = global_config.get('active_plugin', 'librehardwaremonitor')
plugin_config = load_plugin_config(plugin_id)
# Fusionner
merged = global_config.copy()
merged['tabs'] = plugin_config.get('tabs', [])
return merged
def save_merged_config(config):
"""
Sauvegarde une config fusionnée
Sépare les données globales et plugin
"""
global_config = load_config()
plugin_id = global_config.get('active_plugin', 'librehardwaremonitor')
# Extraire les données plugin
plugin_config = {
'tabs': config.get('tabs', [])
}
# Mettre à jour la config globale
global_config['theme'] = config.get('theme', 'dark')
global_config['apps'] = config.get('apps', [])
global_config['apps_tab'] = config.get('apps_tab', '')
# Sauvegarder les deux
save_config(global_config)
save_plugin_config(plugin_id, plugin_config)
# ==================== Gestion du plugin actif ====================
def init_plugin():
"""Initialise le plugin actif"""
global active_plugin
config = load_config()
plugin_id = config.get('active_plugin', 'librehardwaremonitor')
plugin_config = config.get('plugins', {}).get(plugin_id, {})
try:
active_plugin = get_plugin(plugin_id, plugin_config)
print(f"Plugin initialisé: {active_plugin.get_name()}")
except Exception as e:
print(f"Erreur initialisation plugin {plugin_id}: {e}")
# Fallback sur LHM
active_plugin = get_plugin('librehardwaremonitor', {'host': '127.0.0.1', 'port': 8085})
def switch_plugin(plugin_id):
"""Change le plugin actif"""
global active_plugin
config = load_config()
plugin_config = config.get('plugins', {}).get(plugin_id, {})
try:
active_plugin = get_plugin(plugin_id, plugin_config)
config['active_plugin'] = plugin_id
save_config(config)
print(f"Plugin changé: {active_plugin.get_name()}")
return True
except Exception as e:
print(f"Erreur changement plugin: {e}")
return False
# ==================== Thread d'historique ====================
def update_history_loop():
"""Boucle pour mettre à jour l'historique des capteurs"""
global active_plugin
while True:
if active_plugin:
try:
data = active_plugin.get_data()
if data:
sensors = active_plugin.parse_sensors(data)
active_plugin.update_history(sensors)
except Exception as e:
print(f"Erreur update history: {e}")
time.sleep(1)
# ==================== Routes Pages ====================
@app.route('/')
def index():
"""Page d'accueil - redirige vers dashboard"""
return render_template('dashboard.html')
@app.route('/admin')
def admin():
"""Page d'administration"""
return render_template('admin.html')
@app.route('/dashboard')
def dashboard():
"""Page de dashboard"""
return render_template('dashboard.html')
# ==================== API Plugins ====================
@app.route('/api/plugins')
def api_plugins_list():
"""Liste des plugins disponibles"""
plugins = get_available_plugins()
config = load_config()
active_id = config.get('active_plugin', 'librehardwaremonitor')
# Ajouter l'état actif
for plugin in plugins:
plugin['active'] = plugin['id'] == active_id
plugin['config'] = config.get('plugins', {}).get(plugin['id'], {})
return jsonify(plugins)
@app.route('/api/plugins/active')
def api_plugins_active():
"""Retourne le plugin actif"""
global active_plugin
if not active_plugin:
return jsonify({'error': 'Aucun plugin actif'}), 500
config = load_config()
plugin_id = active_plugin.get_id()
return jsonify({
'id': plugin_id,
'name': active_plugin.get_name(),
'config': config.get('plugins', {}).get(plugin_id, {})
})
@app.route('/api/plugins/switch', methods=['POST'])
def api_plugins_switch():
"""Change le plugin actif"""
data = request.get_json()
plugin_id = data.get('plugin_id')
if not plugin_id or plugin_id not in PLUGINS:
return jsonify({'success': False, 'error': 'Plugin invalide'}), 400
if switch_plugin(plugin_id):
return jsonify({'success': True, 'message': f'Plugin changé: {plugin_id}'})
else:
return jsonify({'success': False, 'error': 'Erreur changement plugin'}), 500
@app.route('/api/plugins/config', methods=['POST'])
def api_plugins_config():
"""Met à jour la configuration d'un plugin"""
data = request.get_json()
plugin_id = data.get('plugin_id')
plugin_config = data.get('config', {})
if not plugin_id:
return jsonify({'success': False, 'error': 'Plugin ID requis'}), 400
config = load_config()
if 'plugins' not in config:
config['plugins'] = {}
config['plugins'][plugin_id] = plugin_config
save_config(config)
# Si c'est le plugin actif, le réinitialiser
if active_plugin and active_plugin.get_id() == plugin_id:
init_plugin()
return jsonify({'success': True, 'message': 'Configuration mise à jour'})
@app.route('/api/plugins/test', methods=['POST'])
def api_plugins_test():
"""Teste la connexion d'un plugin"""
data = request.get_json()
plugin_id = data.get('plugin_id')
plugin_config = data.get('config', {})
if not plugin_id or plugin_id not in PLUGINS:
return jsonify({'success': False, 'error': 'Plugin invalide'}), 400
try:
# Créer une instance temporaire pour tester
test_plugin = get_plugin(plugin_id, plugin_config)
result = test_plugin.test_connection()
return jsonify(result)
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== API Capteurs (compatible) ====================
@app.route('/api/lhm/sensors')
def api_lhm_sensors():
"""Retourne la liste complète de tous les capteurs"""
global active_plugin
if not active_plugin:
return jsonify({"error": "Aucun plugin actif"}), 500
data = active_plugin.get_data()
if not data:
return jsonify({"error": "Impossible de récupérer les données"}), 500
sensors = active_plugin.parse_sensors(data)
# Grouper par catégorie
grouped = {}
for sensor in sensors:
category = sensor.get('category', 'Autre')
if category not in grouped:
grouped[category] = []
grouped[category].append(sensor)
return jsonify(grouped)
@app.route('/api/lhm/hierarchy')
def api_lhm_hierarchy():
"""Retourne la structure hiérarchique complète des capteurs"""
global active_plugin
if not active_plugin:
return jsonify({"error": "Aucun plugin actif"}), 500
hierarchy = active_plugin.get_hierarchy()
if not hierarchy:
return jsonify({"error": "Impossible de récupérer les données"}), 500
return jsonify(hierarchy)
@app.route('/api/lhm/data')
def api_lhm_data():
"""Retourne les données actuelles des capteurs sélectionnés"""
global active_plugin
if not active_plugin:
return jsonify({"error": "Aucun plugin actif"}), 500
config = get_merged_config()
data = active_plugin.get_data()
if not data:
return jsonify({"error": "Impossible de récupérer les données"}), 500
all_sensors = active_plugin.parse_sensors(data)
# Créer un dictionnaire pour lookup rapide
sensor_dict = {s['id']: s for s in all_sensors}
# Organiser par onglets
result = {
"tabs": [],
"timestamp": time.time()
}
for tab in config.get('tabs', []):
tab_data = {
"id": tab['id'],
"name": tab['name'],
"sensors": []
}
for sensor_config in tab.get('sensors', []):
sensor_id = sensor_config['id']
if sensor_id in sensor_dict:
sensor = sensor_dict[sensor_id].copy()
sensor['show_graph'] = sensor_config.get('show_graph', False)
sensor['viz_type'] = sensor_config.get('viz_type', 'line')
sensor['card_size'] = sensor_config.get('card_size', 'medium')
sensor['font_family'] = sensor_config.get('font_family', 'system')
sensor['font_bold'] = sensor_config.get('font_bold', True)
sensor['font_size'] = sensor_config.get('font_size', 'small')
sensor['hide_value_mobile'] = sensor_config.get('hide_value_mobile', False)
sensor['show_type_badge'] = sensor_config.get('show_type_badge', True)
sensor['gauge_options'] = sensor_config.get('gauge_options', {})
sensor['chart_options'] = sensor_config.get('chart_options', {})
tab_data['sensors'].append(sensor)
result['tabs'].append(tab_data)
return jsonify(result)
@app.route('/api/lhm/history/<path:sensor_id>')
def api_lhm_history(sensor_id):
"""Retourne l'historique d'un capteur pour le graphique"""
global active_plugin
if not active_plugin:
return jsonify({"error": "Aucun plugin actif"}), 500
# Reconstruire l'ID complet
if not sensor_id.startswith('/'):
sensor_id = '/' + sensor_id
count = request.args.get('count', 60, type=int)
history = active_plugin.get_history(sensor_id, count)
return jsonify({
"sensor_id": sensor_id,
"values": history,
"count": len(history)
})
# ==================== API Configuration ====================
@app.route('/api/config')
def api_config_get():
"""Récupère la configuration actuelle (fusionnée)"""
config = get_merged_config()
return jsonify(config)
@app.route('/api/config', methods=['POST'])
def api_config_save():
"""Sauvegarde la configuration"""
try:
config = request.get_json()
save_merged_config(config)
return jsonify({"success": True, "message": "Configuration sauvegardée"})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# ==================== API Applications ====================
@app.route('/api/apps')
def api_apps_list():
"""Liste des applications configurées"""
config = load_config()
return jsonify(config.get('apps', []))
@app.route('/api/apps/launch/<int:app_id>', methods=['POST'])
def api_apps_launch(app_id):
"""Lance une application"""
config = load_config()
apps = config.get('apps', [])
if app_id < 0 or app_id >= len(apps):
return jsonify({"success": False, "error": "Application non trouvée"}), 404
app_config = apps[app_id]
try:
subprocess.Popen(app_config['path'], shell=True)
return jsonify({"success": True, "message": f"{app_config['name']} lancée"})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# ==================== API Plexamp ====================
def init_plexamp():
"""Initialise le plugin Plexamp si configuré"""
global plexamp_plugin
config = load_config()
plexamp_config = config.get('media', {}).get('plexamp', {})
if plexamp_config.get('enabled') and plexamp_config.get('token'):
try:
plexamp_plugin = get_media_plugin('plexamp', plexamp_config)
print(f"Plexamp initialisé sur {plexamp_config.get('host')}:{plexamp_config.get('port')}")
except Exception as e:
print(f"Erreur initialisation Plexamp: {e}")
plexamp_plugin = None
else:
plexamp_plugin = None
@app.route('/api/plexamp/status')
def api_plexamp_status():
"""Retourne l'état actuel de lecture Plexamp"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"enabled": False, "error": "Plexamp non configuré"})
now_playing = plexamp_plugin.get_now_playing()
if not now_playing:
return jsonify({"enabled": True, "playing": False, "state": "unknown"})
# Ajouter l'URL complète de l'artwork
if now_playing.get('thumb'):
now_playing['artwork_url'] = plexamp_plugin.get_artwork_url(now_playing['thumb'])
now_playing['enabled'] = True
return jsonify(now_playing)
@app.route('/api/plexamp/play', methods=['POST'])
def api_plexamp_play():
"""Reprend la lecture"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"success": False, "error": "Plexamp non configuré"}), 400
success = plexamp_plugin.play()
return jsonify({"success": success})
@app.route('/api/plexamp/pause', methods=['POST'])
def api_plexamp_pause():
"""Met en pause"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"success": False, "error": "Plexamp non configuré"}), 400
success = plexamp_plugin.pause()
return jsonify({"success": success})
@app.route('/api/plexamp/playpause', methods=['POST'])
def api_plexamp_playpause():
"""Toggle play/pause"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"success": False, "error": "Plexamp non configuré"}), 400
success = plexamp_plugin.play_pause()
return jsonify({"success": success})
@app.route('/api/plexamp/next', methods=['POST'])
def api_plexamp_next():
"""Piste suivante"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"success": False, "error": "Plexamp non configuré"}), 400
success = plexamp_plugin.next_track()
return jsonify({"success": success})
@app.route('/api/plexamp/prev', methods=['POST'])
def api_plexamp_prev():
"""Piste précédente"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"success": False, "error": "Plexamp non configuré"}), 400
success = plexamp_plugin.prev_track()
return jsonify({"success": success})
@app.route('/api/plexamp/seek', methods=['POST'])
def api_plexamp_seek():
"""Aller à une position"""
global plexamp_plugin
if not plexamp_plugin:
return jsonify({"success": False, "error": "Plexamp non configuré"}), 400
data = request.get_json()
position = data.get('position', 0)
success = plexamp_plugin.seek(int(position))
return jsonify({"success": success})
@app.route('/api/plexamp/config', methods=['GET'])
def api_plexamp_config_get():
"""Récupère la config Plexamp"""
config = load_config()
plexamp_config = config.get('media', {}).get('plexamp', {})
# Ne pas renvoyer le token complet pour la sécurité
safe_config = {
'enabled': plexamp_config.get('enabled', False),
'host': plexamp_config.get('host', '127.0.0.1'),
'port': plexamp_config.get('port', 32400),
'has_token': bool(plexamp_config.get('token'))
}
return jsonify(safe_config)
@app.route('/api/plexamp/config', methods=['POST'])
def api_plexamp_config_save():
"""Sauvegarde la config Plexamp"""
global plexamp_plugin
data = request.get_json()
config = load_config()
if 'media' not in config:
config['media'] = {}
config['media']['plexamp'] = {
'enabled': data.get('enabled', False),
'host': data.get('host', '127.0.0.1'),
'port': data.get('port', 32400),
'token': data.get('token', '')
}
save_config(config)
# Réinitialiser le plugin
init_plexamp()
return jsonify({"success": True, "message": "Configuration Plexamp sauvegardée"})
@app.route('/api/plexamp/test', methods=['POST'])
def api_plexamp_test():
"""Teste la connexion Plexamp"""
data = request.get_json()
try:
test_plugin = get_media_plugin('plexamp', data)
result = test_plugin.test_connection()
return jsonify(result)
except Exception as e:
return jsonify({"success": False, "error": str(e)})
# ==================== Démarrage ====================
if __name__ == '__main__':
# Initialiser les plugins
init_plugin()
init_plexamp()
# Démarrer le thread d'historique
history_thread = Thread(target=update_history_loop, daemon=True)
history_thread.start()
config = load_config()
plugin_id = config.get('active_plugin', 'librehardwaremonitor')
plugin_config = config.get('plugins', {}).get(plugin_id, {})
plexamp_config = config.get('media', {}).get('plexamp', {})
print("=" * 60)
print("PC Monitor - Serveur de monitoring v1.2.2")
print("=" * 60)
print(f"Plugin actif: {plugin_id}")
print(f"Hôte: {plugin_config.get('host', '127.0.0.1')}")
print(f"Port: {plugin_config.get('port', 'N/A')}")
print("-" * 60)
if plexamp_config.get('enabled'):
print(f"Plexamp: activé ({plexamp_config.get('host')}:{plexamp_config.get('port')})")
else:
print("Plexamp: désactivé")
print("-" * 60)
print(f"Admin: http://localhost:5000/admin")
print(f"Dashboard: http://localhost:5000/dashboard")
print("=" * 60)
app.run(host='0.0.0.0', port=5000, debug=True)

30
config.json Normal file
View File

@@ -0,0 +1,30 @@
{
"active_plugin": "hwinfo",
"theme": "dark",
"apps": [
{
"icon": "🚀",
"name": "plex",
"path": "C:\\Users\\tech\\AppData\\Local\\Programs\\Plexamp\\Plexamp.exe"
}
],
"apps_tab": "tab1770970837781",
"plugins": {
"librehardwaremonitor": {
"host": "127.0.0.1",
"port": 8085
},
"hwinfo": {
"host": "127.0.0.1",
"port": 60000
}
},
"media": {
"plexamp": {
"enabled": true,
"host": "192.168.1.235",
"port": 32400,
"token": "ihycwfjqNtqndjWbziko"
}
}
}

135
config_hwinfo.json Normal file
View File

@@ -0,0 +1,135 @@
{
"tabs": [
{
"id": "tab1",
"name": "Général",
"sensors": [
{
"id": "/hwinfo/GPU_0__NVIDIA_GeForce_RTX_3080_Ti__MSI_RTX_3080_Ti_Ventus_3x_OC_Limite_de_performance_-_Thermique",
"name": "Limite de performance - Thermique",
"show_graph": false,
"type": "percentage",
"viz_type": "none"
},
{
"id": "/hwinfo/Windows_Hardware_Errors_(WHEA)_Total_des_erreurs",
"name": "Total des erreurs",
"show_graph": false,
"type": "generic",
"viz_type": "none"
},
{
"id": "/hwinfo/CPU_0__Intel_Core_i5-14600KF__DTS_CPU_Entier",
"name": "CPU Entier",
"show_graph": false,
"type": "temperature",
"viz_type": "none"
},
{
"id": "/hwinfo/CPU_0__Intel_Core_i5-14600KF__DTS_Étranglement_thermique_de_paquet_anneau",
"name": "Étranglement thermique de paquet/anneau",
"show_graph": false,
"type": "percentage",
"viz_type": "none"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/hwinfo/GPU_0__NVIDIA_GeForce_RTX_3080_Ti__MSI_RTX_3080_Ti_Ventus_3x_OC_Température_GPU",
"name": "Température GPU",
"show_graph": true,
"show_type_badge": false,
"type": "temperature",
"viz_type": "line"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "small",
"hide_value_mobile": false,
"id": "/hwinfo/0_134217729_Mémoire_virtuelle_disponible",
"name": "Mémoire virtuelle disponible",
"show_graph": true,
"show_type_badge": false,
"type": "generic",
"viz_type": "line"
},
{
"card_size": "tiny",
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/hwinfo/0_134217730_Charge_de_mémoire_virtuelle",
"name": "Charge de mémoire virtuelle",
"show_graph": false,
"show_type_badge": false,
"type": "percentage",
"viz_type": "none"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/hwinfo/2_134217740_Étranglement_thermique_de_paquet_anneau",
"name": "Étranglement thermique de paquet/anneau",
"show_graph": false,
"show_type_badge": false,
"type": "generic",
"viz_type": "none"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"gauge_options": {
"show_value": true,
"size": "xs",
"style": "semicircle"
},
"hide_value_mobile": false,
"id": "/hwinfo/22_16777216_Diode_thermique_GPU",
"name": "Diode thermique GPU",
"show_graph": true,
"show_type_badge": false,
"type": "temperature",
"viz_type": "gauge"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/hwinfo/2_16777470_CPU_Entier",
"name": "CPU Entier",
"show_graph": true,
"show_type_badge": false,
"type": "temperature",
"viz_type": "line"
}
]
},
{
"id": "tab1770970837781",
"name": "apps",
"sensors": []
}
]
}

View File

@@ -0,0 +1,240 @@
{
"tabs": [
{
"id": "tab1",
"name": "Général",
"sensors": [
{
"card_size": "tiny",
"font_bold": false,
"font_family": "opensans",
"font_size": "tiny",
"hide_value_mobile": false,
"id": "/lpc/nct6687d/0/fan/0",
"name": "CPU Fan",
"show_graph": false,
"show_type_badge": false,
"type": "fan",
"viz_type": "none"
},
{
"card_size": "tiny",
"font_bold": false,
"font_family": "opensans",
"font_size": "tiny",
"gauge_options": {
"show_value": false,
"size": "xs",
"style": "semicircle"
},
"hide_value_mobile": false,
"id": "/intelcpu/0/power/0",
"name": "CPU Package",
"show_graph": false,
"show_type_badge": false,
"type": "power",
"viz_type": "none"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/intelcpu/0/load/0",
"name": "CPU Total",
"show_graph": false,
"show_type_badge": false,
"type": "percentage",
"viz_type": "none"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "system",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/ram/load/0",
"name": "Memory",
"show_graph": false,
"show_type_badge": false,
"type": "percentage",
"viz_type": "none"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "system",
"font_size": "small",
"hide_value_mobile": false,
"id": "/vram/data/2",
"name": "Memory Used",
"show_graph": false,
"show_type_badge": false,
"type": "memory",
"viz_type": "none"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": true,
"font_family": "opensans",
"font_size": "large",
"hide_value_mobile": false,
"id": "/gpu-nvidia/0/power/0",
"name": "GPU Package",
"show_graph": true,
"show_type_badge": false,
"type": "power",
"viz_type": "line"
},
{
"card_size": "small",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "xs",
"gauge_options": {
"show_value": false,
"size": "xs",
"style": "arc"
},
"hide_value_mobile": false,
"id": "/gpu-nvidia/0/clock/4",
"name": "GPU Memory",
"show_graph": true,
"show_type_badge": false,
"type": "frequency",
"viz_type": "line"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "system",
"font_size": "xs",
"hide_value_mobile": false,
"id": "/lpc/nct6687d/0/temperature/0",
"name": "CPU",
"show_graph": true,
"show_type_badge": false,
"type": "temperature",
"viz_type": "line"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "opensans",
"font_size": "small",
"gauge_options": {
"show_value": false,
"size": "tiny",
"style": "arc"
},
"hide_value_mobile": false,
"id": "/gpu-nvidia/0/clock/0",
"name": "GPU Core",
"show_graph": true,
"show_type_badge": false,
"type": "frequency",
"viz_type": "gauge"
},
{
"card_size": "xs",
"font_bold": false,
"font_family": "roboto",
"font_size": "xs",
"gauge_options": {
"show_value": false,
"size": "tiny",
"style": "arc"
},
"hide_value_mobile": false,
"id": "/gpu-nvidia/0/temperature/0",
"name": "GPU Core",
"show_graph": true,
"show_type_badge": false,
"type": "temperature",
"viz_type": "gauge"
},
{
"card_size": "tiny",
"font_bold": true,
"font_family": "opensans",
"font_size": "tiny",
"gauge_options": {
"show_value": false,
"size": "xs",
"style": "semicircle"
},
"hide_value_mobile": false,
"id": "/gpu-nvidia/0/smalldata/1",
"name": "GPU Memory Used",
"show_graph": false,
"show_type_badge": false,
"type": "memory",
"viz_type": "none"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "tiny",
"hide_value_mobile": false,
"id": "/nic/%7BD712B4D3-745F-4969-BC51-728433D0605D%7D/load/1",
"name": "Network Utilization",
"show_graph": true,
"show_type_badge": false,
"type": "percentage",
"viz_type": "line"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "small",
"hide_value_mobile": false,
"id": "/nic/%7BD712B4D3-745F-4969-BC51-728433D0605D%7D/throughput/7",
"name": "Upload Speed",
"show_graph": true,
"show_type_badge": false,
"type": "percentage",
"viz_type": "line"
},
{
"card_size": "xs",
"chart_options": {
"height": "xs"
},
"font_bold": false,
"font_family": "opensans",
"font_size": "small",
"hide_value_mobile": false,
"id": "/nic/%7BD712B4D3-745F-4969-BC51-728433D0605D%7D/throughput/8",
"name": "Download Speed",
"show_graph": true,
"show_type_badge": false,
"type": "percentage",
"viz_type": "line"
}
]
},
{
"id": "tab2",
"name": "Application",
"sensors": []
}
]
}

113
diagnostic.py Normal file
View File

@@ -0,0 +1,113 @@
"""
Script de diagnostic pour tester la connexion à LibreHardwareMonitor
"""
import socket
import urllib.request
import sys
print("=" * 60)
print("DIAGNOSTIC DE CONNEXION À LIBREHARDWAREMONITOR")
print("=" * 60)
# Test 1: Résolution DNS de localhost
print("\n[1] Résolution DNS de 'localhost'...")
try:
ip = socket.gethostbyname('localhost')
print(f" ✅ localhost résolu en: {ip}")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 2: Vérifier toutes les adresses pour localhost
print("\n[2] Toutes les adresses pour 'localhost'...")
try:
infos = socket.getaddrinfo('localhost', 8085)
for info in infos:
print(f"{info[0].name}: {info[4]}")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 3: Connexion socket directe sur 127.0.0.1
print("\n[3] Test connexion socket sur 127.0.0.1:8085...")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
result = sock.connect_ex(('127.0.0.1', 8085))
sock.close()
if result == 0:
print(" ✅ Port 8085 ouvert sur 127.0.0.1")
else:
print(f" ❌ Port fermé (code: {result})")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 4: Connexion socket directe sur localhost
print("\n[4] Test connexion socket sur localhost:8085...")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
result = sock.connect_ex(('localhost', 8085))
sock.close()
if result == 0:
print(" ✅ Port 8085 ouvert sur localhost")
else:
print(f" ❌ Port fermé (code: {result})")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 5: Requête HTTP avec urllib (sans requests)
print("\n[5] Test HTTP avec urllib sur 127.0.0.1...")
try:
req = urllib.request.urlopen('http://127.0.0.1:8085/data.json', timeout=5)
data = req.read()
print(f" ✅ Réponse reçue! ({len(data)} bytes)")
print(f" Début: {data[:100]}...")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 6: Requête HTTP avec urllib sur localhost
print("\n[6] Test HTTP avec urllib sur localhost...")
try:
req = urllib.request.urlopen('http://localhost:8085/data.json', timeout=5)
data = req.read()
print(f" ✅ Réponse reçue! ({len(data)} bytes)")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 7: Requête avec requests (si disponible)
print("\n[7] Test HTTP avec requests sur 127.0.0.1...")
try:
import requests
resp = requests.get('http://127.0.0.1:8085/data.json', timeout=5)
print(f" ✅ Réponse reçue! Status: {resp.status_code}, {len(resp.content)} bytes")
except ImportError:
print(" ⚠️ Module requests non installé")
except Exception as e:
print(f" ❌ Erreur: {e}")
# Test 8: Requête avec requests sur localhost
print("\n[8] Test HTTP avec requests sur localhost...")
try:
import requests
resp = requests.get('http://localhost:8085/data.json', timeout=5)
print(f" ✅ Réponse reçue! Status: {resp.status_code}")
except ImportError:
print(" ⚠️ Module requests non installé")
except Exception as e:
print(f" ❌ Erreur: {e}")
print("\n" + "=" * 60)
print("RECOMMANDATION:")
print("=" * 60)
print("""
Si les tests 3-4 échouent mais que le navigateur fonctionne:
→ Vérifiez le pare-feu Windows (autorisez Python)
Si les tests avec 127.0.0.1 réussissent mais localhost échoue:
→ Problème IPv6, utilisez 127.0.0.1 dans la config
Si tout échoue sauf le navigateur:
→ Un proxy ou antivirus bloque peut-être Python
→ Essayez de désactiver temporairement l'antivirus
""")
input("\nAppuyez sur Entrée pour fermer...")

Binary file not shown.

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,92 @@
Metadata-Version: 2.1
Name: MarkupSafe
Version: 3.0.2
Summary: Safely add untrusted strings to HTML/XML markup.
Maintainer-email: Pallets <contact@palletsprojects.com>
License: Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
Project-URL: Source, https://github.com/pallets/markupsafe/
Project-URL: Chat, https://discord.gg/pallets
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Text Processing :: Markup :: HTML
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE.txt
# MarkupSafe
MarkupSafe implements a text object that escapes characters so it is
safe to use in HTML and XML. Characters that have special meanings are
replaced so that they display as the actual characters. This mitigates
injection attacks, meaning untrusted user input can safely be displayed
on a page.
## Examples
```pycon
>>> from markupsafe import Markup, escape
>>> # escape replaces special characters and wraps in Markup
>>> escape("<script>alert(document.cookie);</script>")
Markup('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
>>> # wrap in Markup to mark text "safe" and prevent escaping
>>> Markup("<strong>Hello</strong>")
Markup('<strong>hello</strong>')
>>> escape(Markup("<strong>Hello</strong>"))
Markup('<strong>hello</strong>')
>>> # Markup is a str subclass
>>> # methods and operators escape their arguments
>>> template = Markup("Hello <em>{name}</em>")
>>> template.format(name='"World"')
Markup('Hello <em>&#34;World&#34;</em>')
```
## Donate
The Pallets organization develops and supports MarkupSafe and other
popular packages. In order to grow the community of contributors and
users, and allow the maintainers to devote more time to the projects,
[please donate today][].
[please donate today]: https://palletsprojects.com/donate

View File

@@ -0,0 +1,14 @@
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=RjHsDbX9kKVH4zaBcmTGeYIUM4FG-KyUtKV_lu6MnsQ,1503
MarkupSafe-3.0.2.dist-info/METADATA,sha256=nhoabjupBG41j_JxPCJ3ylgrZ6Fx8oMCFbiLF9Kafqc,4067
MarkupSafe-3.0.2.dist-info/RECORD,,
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=-v_yZ08fSknsoT62oIKG9wp1eCBV9_ao2rO4BeIReTY,101
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
markupsafe/__init__.py,sha256=pREerPwvinB62tNCMOwqxBS2YHV6R52Wcq1d-rB4Z5o,13609
markupsafe/__pycache__/__init__.cpython-313.pyc,,
markupsafe/__pycache__/_native.cpython-313.pyc,,
markupsafe/_native.py,sha256=2ptkJ40yCcp9kq3L1NqpgjfpZB-obniYKFFKUOkHh4Q,218
markupsafe/_speedups.c,sha256=SglUjn40ti9YgQAO--OgkSyv9tXq9vvaHyVhQows4Ok,4353
markupsafe/_speedups.cp313-win_amd64.pyd,sha256=7MA12j0aUiSeNpFy-98h_pPSqgCpLeRacgp3I-j00Yo,13312
markupsafe/_speedups.pyi,sha256=LSDmXYOefH4HVpAXuL8sl7AttLw0oXh1njVoVZp2wqQ,42
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

View File

@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (75.2.0)
Root-Is-Purelib: false
Tag: cp313-cp313-win_amd64

View File

@@ -0,0 +1 @@
markupsafe

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,82 @@
Metadata-Version: 2.4
Name: click
Version: 8.2.1
Summary: Composable command line interface toolkit
Maintainer-email: Pallets <contact@palletsprojects.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-Expression: BSD-3-Clause
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Typing :: Typed
License-File: LICENSE.txt
Requires-Dist: colorama; platform_system == 'Windows'
Project-URL: Changes, https://click.palletsprojects.com/page/changes/
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://click.palletsprojects.com/
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Source, https://github.com/pallets/click/
# $ click_
Click is a Python package for creating beautiful command line interfaces
in a composable way with as little code as necessary. It's the "Command
Line Interface Creation Kit". It's highly configurable but comes with
sensible defaults out of the box.
It aims to make the process of writing command line tools quick and fun
while also preventing any frustration caused by the inability to
implement an intended CLI API.
Click in three points:
- Arbitrary nesting of commands
- Automatic help page generation
- Supports lazy loading of subcommands at runtime
## A Simple Example
```python
import click
@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == '__main__':
hello()
```
```
$ python hello.py --count=3
Your name: Click
Hello, Click!
Hello, Click!
Hello, Click!
```
## Donate
The Pallets organization develops and supports Click and other popular
packages. In order to grow the community of contributors and users, and
allow the maintainers to devote more time to the projects, [please
donate today][].
[please donate today]: https://palletsprojects.com/donate
## Contributing
See our [detailed contributing documentation][contrib] for many ways to
contribute, including reporting issues, requesting features, asking or answering
questions, and making PRs.
[contrib]: https://palletsprojects.com/contributing/

View File

@@ -0,0 +1,38 @@
click-8.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
click-8.2.1.dist-info/METADATA,sha256=dI1MbhHTLoKD2tNCCGnx9rK2gok23HDNylFeLKdLSik,2471
click-8.2.1.dist-info/RECORD,,
click-8.2.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
click-8.2.1.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475
click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473
click/__pycache__/__init__.cpython-313.pyc,,
click/__pycache__/_compat.cpython-313.pyc,,
click/__pycache__/_termui_impl.cpython-313.pyc,,
click/__pycache__/_textwrap.cpython-313.pyc,,
click/__pycache__/_winconsole.cpython-313.pyc,,
click/__pycache__/core.cpython-313.pyc,,
click/__pycache__/decorators.cpython-313.pyc,,
click/__pycache__/exceptions.cpython-313.pyc,,
click/__pycache__/formatting.cpython-313.pyc,,
click/__pycache__/globals.cpython-313.pyc,,
click/__pycache__/parser.cpython-313.pyc,,
click/__pycache__/shell_completion.cpython-313.pyc,,
click/__pycache__/termui.cpython-313.pyc,,
click/__pycache__/testing.cpython-313.pyc,,
click/__pycache__/types.cpython-313.pyc,,
click/__pycache__/utils.cpython-313.pyc,,
click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693
click/_termui_impl.py,sha256=ASXhLi9IQIc0Js9KQSS-3-SLZcPet3VqysBf9WgbbpI,26712
click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400
click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465
click/core.py,sha256=gUhpNS9cFBGdEXXdisGVG-eRvGf49RTyFagxulqwdFw,117343
click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461
click/exceptions.py,sha256=1rdtXgHJ1b3OjGkN-UpXB9t_HCBihJvh_DtpmLmwn9s,9891
click/formatting.py,sha256=Bhqx4QXdKQ9W4WKknIwj5KPKFmtduGOuGq1yw_THLZ8,9726
click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923
click/parser.py,sha256=nU1Ah2p11q29ul1vNdU9swPo_PUuKrxU6YXToi71q1c,18979
click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
click/shell_completion.py,sha256=CQSGdjgun4ORbOZrXP0CVhEtPx4knsufOkRsDiK64cM,19857
click/termui.py,sha256=vAYrKC2a7f_NfEIhAThEVYfa__ib5XQbTSCGtJlABRA,30847
click/testing.py,sha256=2eLdAaCJCGToP5Tw-XN8JjrDb3wbJIfARxg3d0crW5M,18702
click/types.py,sha256=KBTRxN28cR1VZ5mb9iJX98MQSw_p9SGzljqfEI8z5Tw,38389
click/utils.py,sha256=b1Mm-usEDBHtEwcPltPIn3zSK4nw2KTp5GC7_oSTlLo,20245

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.12.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,28 @@
Copyright 2014 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,116 @@
Metadata-Version: 2.1
Name: Flask
Version: 3.0.0
Summary: A simple framework for building complex web applications.
Maintainer-email: Pallets <contact@palletsprojects.com>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Dist: Werkzeug>=3.0.0
Requires-Dist: Jinja2>=3.1.2
Requires-Dist: itsdangerous>=2.1.2
Requires-Dist: click>=8.1.3
Requires-Dist: blinker>=1.6.2
Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10'
Requires-Dist: asgiref>=3.2 ; extra == "async"
Requires-Dist: python-dotenv ; extra == "dotenv"
Project-URL: Changes, https://flask.palletsprojects.com/changes/
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://flask.palletsprojects.com/
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Issue Tracker, https://github.com/pallets/flask/issues/
Project-URL: Source Code, https://github.com/pallets/flask/
Provides-Extra: async
Provides-Extra: dotenv
Flask
=====
Flask is a lightweight `WSGI`_ web application framework. It is designed
to make getting started quick and easy, with the ability to scale up to
complex applications. It began as a simple wrapper around `Werkzeug`_
and `Jinja`_ and has become one of the most popular Python web
application frameworks.
Flask offers suggestions, but doesn't enforce any dependencies or
project layout. It is up to the developer to choose the tools and
libraries they want to use. There are many extensions provided by the
community that make adding new functionality easy.
.. _WSGI: https://wsgi.readthedocs.io/
.. _Werkzeug: https://werkzeug.palletsprojects.com/
.. _Jinja: https://jinja.palletsprojects.com/
Installing
----------
Install and update using `pip`_:
.. code-block:: text
$ pip install -U Flask
.. _pip: https://pip.pypa.io/en/stable/getting-started/
A Simple Example
----------------
.. code-block:: python
# save this as app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, World!"
.. code-block:: text
$ flask run
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Contributing
------------
For guidance on setting up a development environment and how to make a
contribution to Flask, see the `contributing guidelines`_.
.. _contributing guidelines: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst
Donate
------
The Pallets organization develops and supports Flask and the libraries
it uses. In order to grow the community of contributors and users, and
allow the maintainers to devote more time to the projects, `please
donate today`_.
.. _please donate today: https://palletsprojects.com/donate
Links
-----
- Documentation: https://flask.palletsprojects.com/
- Changes: https://flask.palletsprojects.com/changes/
- PyPI Releases: https://pypi.org/project/Flask/
- Source Code: https://github.com/pallets/flask/
- Issue Tracker: https://github.com/pallets/flask/issues/
- Chat: https://discord.gg/pallets

View File

@@ -0,0 +1,58 @@
../../Scripts/flask.exe,sha256=ND9f0z8XN9fqR_S6jQEm-9esibL5LMMT-NWKLivt0pI,108381
flask-3.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
flask-3.0.0.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
flask-3.0.0.dist-info/METADATA,sha256=02XP69VTiwn5blcRgHcyuSQ2cLTuJFV8FXw2x4QnxKo,3588
flask-3.0.0.dist-info/RECORD,,
flask-3.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
flask-3.0.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
flask-3.0.0.dist-info/entry_points.txt,sha256=bBP7hTOS5fz9zLtC7sPofBZAlMkEvBxu7KqS6l5lvc4,40
flask/__init__.py,sha256=6xMqdVA0FIQ2U1KVaGX3lzNCdXPzoHPaa0hvQCNcfSk,2625
flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
flask/__pycache__/__init__.cpython-313.pyc,,
flask/__pycache__/__main__.cpython-313.pyc,,
flask/__pycache__/app.cpython-313.pyc,,
flask/__pycache__/blueprints.cpython-313.pyc,,
flask/__pycache__/cli.cpython-313.pyc,,
flask/__pycache__/config.cpython-313.pyc,,
flask/__pycache__/ctx.cpython-313.pyc,,
flask/__pycache__/debughelpers.cpython-313.pyc,,
flask/__pycache__/globals.cpython-313.pyc,,
flask/__pycache__/helpers.cpython-313.pyc,,
flask/__pycache__/logging.cpython-313.pyc,,
flask/__pycache__/sessions.cpython-313.pyc,,
flask/__pycache__/signals.cpython-313.pyc,,
flask/__pycache__/templating.cpython-313.pyc,,
flask/__pycache__/testing.cpython-313.pyc,,
flask/__pycache__/typing.cpython-313.pyc,,
flask/__pycache__/views.cpython-313.pyc,,
flask/__pycache__/wrappers.cpython-313.pyc,,
flask/app.py,sha256=voUkc9xk9B039AhVrU21GDpsQ6wqrr-NobqLx8fURfQ,59201
flask/blueprints.py,sha256=zO8bLO9Xy1aVD92bDmzihutjVEXf8xdDaVfiy7c--Ck,3129
flask/cli.py,sha256=PDwZCfPagi5GUzb-D6dEN7y20gWiVAg3ejRnxBKNHPA,33821
flask/config.py,sha256=YZSZ-xpFj1iW1B1Kj1iDhpc5s7pHncloiRLqXhsU7Hs,12856
flask/ctx.py,sha256=x2kGzUXtPzVyi2YSKrU_PV1AvtxTmh2iRdriJRTSPGM,14841
flask/debughelpers.py,sha256=WKzD2FNTSimNSwCJVLr9_fFo1f2VlTWB5EZ6lmR5bwE,5548
flask/globals.py,sha256=XdQZmStBmPIs8t93tjx6pO7Bm3gobAaONWkFcUHaGas,1713
flask/helpers.py,sha256=ynEoMB7fdF5Y1P-ngxMjZDZWfrJ4St-9OGZZsTcUwx8,22992
flask/json/__init__.py,sha256=pdtpoK2b0b1u7Sxbx3feM7VWhsI20l1yGAvbYWxaxvc,5572
flask/json/__pycache__/__init__.cpython-313.pyc,,
flask/json/__pycache__/provider.cpython-313.pyc,,
flask/json/__pycache__/tag.cpython-313.pyc,,
flask/json/provider.py,sha256=VBKSK75t3OsTvZ3N10B3Fsu7-NdpfrGYcl41goQJ3q8,7640
flask/json/tag.py,sha256=ihb7QWrNEr0YC3KD4TolZbftgSPCuLk7FAvK49huYC0,8871
flask/logging.py,sha256=VcdJgW4Axm5l_-7vXLQjRTL0eckaMks7Ya_HaoDm0wg,2330
flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
flask/sansio/README.md,sha256=-0X1tECnilmz1cogx-YhNw5d7guK7GKrq_DEV2OzlU0,228
flask/sansio/__pycache__/app.cpython-313.pyc,,
flask/sansio/__pycache__/blueprints.cpython-313.pyc,,
flask/sansio/__pycache__/scaffold.cpython-313.pyc,,
flask/sansio/app.py,sha256=nZWCFMOW8qK95Ck9UvDzxvswQr-coLJhIFaa_OVobCc,37977
flask/sansio/blueprints.py,sha256=caskVI1Zf3mM5msevK5-tWy3VqX_A8mlB0KGNyRx5_0,24319
flask/sansio/scaffold.py,sha256=-Cus0cVS4PmLof4qLvfjSQzk4AKsLqPR6LBpv6ALw3Y,30580
flask/sessions.py,sha256=rFH2QKXG24dEazkKGxAHqUpAUh_30hDHrddhVYgAcY0,14169
flask/signals.py,sha256=V7lMUww7CqgJ2ThUBn1PiatZtQanOyt7OZpu2GZI-34,750
flask/templating.py,sha256=EtL8CE5z2aefdR1I-TWYVNg0cSuXBqz_lvOGKeggktk,7538
flask/testing.py,sha256=h7AinggrMgGzKlDN66VfB0JjWW4Z1U_OD6FyjqBNiYM,10017
flask/typing.py,sha256=2pGlhSaZqJVJOoh-QdH-20QVzl2r-zLXyP8otXfCCs4,3156
flask/views.py,sha256=V5hOGZLx0Bn99QGcM6mh5x_uM-MypVT0-RysEFU84jc,6789
flask/wrappers.py,sha256=PhMp3teK3SnEmIdog59cO_DHiZ9Btn0qI1EifrTdwP8,5709

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.9.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,3 @@
[console_scripts]
flask=flask.cli:main

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,28 @@
Copyright 2011 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,60 @@
Metadata-Version: 2.1
Name: itsdangerous
Version: 2.2.0
Summary: Safely pass data to untrusted environments and back.
Maintainer-email: Pallets <contact@palletsprojects.com>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Typing :: Typed
Project-URL: Changes, https://itsdangerous.palletsprojects.com/changes/
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://itsdangerous.palletsprojects.com/
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Source, https://github.com/pallets/itsdangerous/
# ItsDangerous
... so better sign this
Various helpers to pass data to untrusted environments and to get it
back safe and sound. Data is cryptographically signed to ensure that a
token has not been tampered with.
It's possible to customize how data is serialized. Data is compressed as
needed. A timestamp can be added and verified automatically while
loading a token.
## A Simple Example
Here's how you could generate a token for transmitting a user's id and
name between web requests.
```python
from itsdangerous import URLSafeSerializer
auth_s = URLSafeSerializer("secret key", "auth")
token = auth_s.dumps({"id": 5, "name": "itsdangerous"})
print(token)
# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg
data = auth_s.loads(token)
print(data["name"])
# itsdangerous
```
## Donate
The Pallets organization develops and supports ItsDangerous and other
popular packages. In order to grow the community of contributors and
users, and allow the maintainers to devote more time to the projects,
[please donate today][].
[please donate today]: https://palletsprojects.com/donate

View File

@@ -0,0 +1,22 @@
itsdangerous-2.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
itsdangerous-2.2.0.dist-info/LICENSE.txt,sha256=Y68JiRtr6K0aQlLtQ68PTvun_JSOIoNnvtfzxa4LCdc,1475
itsdangerous-2.2.0.dist-info/METADATA,sha256=0rk0-1ZwihuU5DnwJVwPWoEI4yWOyCexih3JyZHblhE,1924
itsdangerous-2.2.0.dist-info/RECORD,,
itsdangerous-2.2.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
itsdangerous/__init__.py,sha256=4SK75sCe29xbRgQE1ZQtMHnKUuZYAf3bSpZOrff1IAY,1427
itsdangerous/__pycache__/__init__.cpython-313.pyc,,
itsdangerous/__pycache__/_json.cpython-313.pyc,,
itsdangerous/__pycache__/encoding.cpython-313.pyc,,
itsdangerous/__pycache__/exc.cpython-313.pyc,,
itsdangerous/__pycache__/serializer.cpython-313.pyc,,
itsdangerous/__pycache__/signer.cpython-313.pyc,,
itsdangerous/__pycache__/timed.cpython-313.pyc,,
itsdangerous/__pycache__/url_safe.cpython-313.pyc,,
itsdangerous/_json.py,sha256=wPQGmge2yZ9328EHKF6gadGeyGYCJQKxtU-iLKE6UnA,473
itsdangerous/encoding.py,sha256=wwTz5q_3zLcaAdunk6_vSoStwGqYWe307Zl_U87aRFM,1409
itsdangerous/exc.py,sha256=Rr3exo0MRFEcPZltwecyK16VV1bE2K9_F1-d-ljcUn4,3201
itsdangerous/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
itsdangerous/serializer.py,sha256=PmdwADLqkSyQLZ0jOKAgDsAW4k_H0TlA71Ei3z0C5aI,15601
itsdangerous/signer.py,sha256=YO0CV7NBvHA6j549REHJFUjUojw2pHqwcUpQnU7yNYQ,9647
itsdangerous/timed.py,sha256=6RvDMqNumGMxf0-HlpaZdN9PUQQmRvrQGplKhxuivUs,8083
itsdangerous/url_safe.py,sha256=az4e5fXi_vs-YbWj8YZwn4wiVKfeD--GEKRT5Ueu4P4,2505

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.9.0
Root-Is-Purelib: true
Tag: py3-none-any

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,35 @@
# Plugins pour PC Monitor
from .base import BasePlugin
from .librehardwaremonitor import LibreHardwareMonitorPlugin
from .hwinfo import HWiNFOPlugin
# Registry des plugins disponibles
PLUGINS = {
'librehardwaremonitor': LibreHardwareMonitorPlugin,
'hwinfo': HWiNFOPlugin
}
def get_plugin(name: str, config: dict) -> BasePlugin:
"""Retourne une instance du plugin demandé"""
if name not in PLUGINS:
raise ValueError(f"Plugin inconnu: {name}")
return PLUGINS[name](config)
def get_available_plugins() -> list:
"""Retourne la liste des plugins disponibles"""
return [
{
'id': 'librehardwaremonitor',
'name': 'LibreHardwareMonitor',
'description': 'Open source, API REST intégrée',
'default_port': 8085,
'website': 'https://github.com/LibreHardwareMonitor/LibreHardwareMonitor'
},
{
'id': 'hwinfo',
'name': 'HWiNFO + RemoteHWInfo',
'description': 'HWiNFO avec RemoteHWInfo (github.com/Demion/remotehwinfo)',
'default_port': 60000,
'website': 'https://github.com/Demion/remotehwinfo'
}
]

View File

@@ -0,0 +1,151 @@
"""
Classe de base pour tous les plugins de monitoring
"""
from abc import ABC, abstractmethod
from typing import Optional, List, Dict, Any
class BasePlugin(ABC):
"""Classe abstraite définissant l'interface des plugins"""
def __init__(self, config: dict):
"""
Initialise le plugin avec sa configuration
Args:
config: Configuration du plugin (host, port, etc.)
"""
self.host = config.get('host', '127.0.0.1')
self.port = config.get('port', self.get_default_port())
self.config = config
@abstractmethod
def get_id(self) -> str:
"""Retourne l'identifiant unique du plugin"""
pass
@abstractmethod
def get_name(self) -> str:
"""Retourne le nom affichable du plugin"""
pass
@abstractmethod
def get_default_port(self) -> int:
"""Retourne le port par défaut"""
pass
@abstractmethod
def test_connection(self) -> Dict[str, Any]:
"""
Teste la connexion au logiciel de monitoring
Returns:
dict avec 'success' (bool), 'message' (str), et optionnellement 'version'
"""
pass
@abstractmethod
def get_data(self) -> Optional[dict]:
"""
Récupère les données brutes du logiciel de monitoring
Returns:
Données brutes ou None si erreur
"""
pass
@abstractmethod
def get_hierarchy(self) -> List[dict]:
"""
Récupère la hiérarchie des capteurs (pour l'admin)
Returns:
Liste de hardware avec leurs capteurs
"""
pass
@abstractmethod
def parse_sensors(self, data: dict) -> List[dict]:
"""
Parse les données brutes en liste standardisée de capteurs
Args:
data: Données brutes du logiciel
Returns:
Liste de capteurs au format standardisé:
[
{
'id': str, # Identifiant unique
'name': str, # Nom du capteur
'value': str, # Valeur formatée (ex: "45 °C")
'raw_value': float, # Valeur numérique
'type': str, # Type (temperature, load, voltage, etc.)
'unit': str, # Unité (°C, %, MHz, etc.)
'hardware': str, # Nom du hardware parent
'hardware_type': str # Type de hardware (CPU, GPU, etc.)
},
...
]
"""
pass
def get_base_url(self) -> str:
"""Retourne l'URL de base pour les requêtes"""
return f"http://{self.host}:{self.port}"
def get_sensor_type(self, unit: str, name: str = '') -> str:
"""
Détermine le type de capteur à partir de l'unité et du nom
Args:
unit: Unité du capteur
name: Nom du capteur (pour affiner la détection)
Returns:
Type standardisé du capteur
"""
unit_lower = unit.lower() if unit else ''
name_lower = name.lower() if name else ''
# Détection par unité
if '°c' in unit_lower or '°f' in unit_lower:
return 'temperature'
elif '%' in unit_lower:
if 'load' in name_lower or 'usage' in name_lower:
return 'load'
return 'percentage'
elif 'mhz' in unit_lower or 'ghz' in unit_lower:
return 'frequency'
elif 'rpm' in unit_lower:
return 'fan'
elif 'w' in unit_lower and 'wh' not in unit_lower:
return 'power'
elif 'v' in unit_lower and 'mv' not in unit_lower:
return 'voltage'
elif 'mv' in unit_lower:
return 'voltage'
elif 'mb' in unit_lower or 'gb' in unit_lower or 'kb' in unit_lower:
return 'data'
elif 'mb/s' in unit_lower or 'kb/s' in unit_lower or 'gb/s' in unit_lower:
return 'throughput'
elif 'a' in unit_lower:
return 'current'
elif 'wh' in unit_lower:
return 'energy'
# Détection par nom si unité non reconnue
if 'temp' in name_lower or 'temperature' in name_lower:
return 'temperature'
elif 'fan' in name_lower:
return 'fan'
elif 'clock' in name_lower or 'freq' in name_lower:
return 'frequency'
elif 'load' in name_lower or 'usage' in name_lower:
return 'load'
elif 'power' in name_lower:
return 'power'
elif 'voltage' in name_lower or 'vcore' in name_lower:
return 'voltage'
return 'generic'

View File

@@ -0,0 +1,274 @@
"""
Plugin pour HWiNFO via RemoteHWInfo (Demion)
https://github.com/Demion/remotehwinfo
Format JSON RemoteHWInfo:
{
"hwinfo": {
"sensors": [
{"entryIndex": 0, "sensorNameUser": "System", ...},
{"entryIndex": 1, "sensorNameUser": "CPU [#0]: Intel Core i7", ...}
],
"readings": [
{"sensorIndex": 0, "labelUser": "Memory Load", "value": 45.5, "unit": "%", ...},
{"sensorIndex": 1, "labelUser": "CPU Temp", "value": 65.0, "unit": "°C", ...}
]
}
}
"""
import requests
from typing import Dict, List, Optional, Any
from .base import BasePlugin
class HWiNFOPlugin(BasePlugin):
"""Plugin pour HWiNFO via RemoteHWInfo"""
def __init__(self, config: dict):
super().__init__(config)
self.history = {}
self.max_history = 120
def get_id(self) -> str:
return 'hwinfo'
def get_name(self) -> str:
return 'HWiNFO'
def get_default_port(self) -> int:
return 60000
def get_base_url(self) -> str:
"""Retourne l'URL de l'API RemoteHWInfo"""
return f"http://{self.host}:{self.port}/json.json"
def test_connection(self) -> Dict[str, Any]:
"""Teste la connexion à RemoteHWInfo"""
try:
url = self.get_base_url()
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
# Vérifier le format RemoteHWInfo
if 'hwinfo' not in data:
return {
'success': False,
'message': 'Format invalide. Vérifiez que RemoteHWInfo est lancé.'
}
hwinfo = data['hwinfo']
sensor_count = len(hwinfo.get('sensors', []))
reading_count = len(hwinfo.get('readings', []))
return {
'success': True,
'message': f'Connecté - {sensor_count} capteurs, {reading_count} lectures',
'version': 'RemoteHWInfo',
'sensor_count': reading_count
}
except requests.exceptions.ConnectionError:
return {
'success': False,
'message': f'Impossible de se connecter à {self.get_base_url()}. Vérifiez que RemoteHWInfo est lancé.'
}
except requests.exceptions.Timeout:
return {
'success': False,
'message': 'Timeout - Le serveur ne répond pas'
}
except Exception as e:
return {
'success': False,
'message': f'Erreur: {str(e)}'
}
def get_data(self) -> Optional[Dict]:
"""Récupère les données JSON depuis RemoteHWInfo"""
try:
url = self.get_base_url()
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
if 'hwinfo' in data:
return data['hwinfo']
return None
except Exception as e:
print(f"Erreur HWiNFO get_data: {e}")
return None
def get_hierarchy(self) -> List[dict]:
"""Récupère la hiérarchie des capteurs pour l'admin"""
data = self.get_data()
if not data:
return []
return self._build_hierarchy(data)
def _build_hierarchy(self, data: Dict) -> List[dict]:
"""Construit la hiérarchie des capteurs"""
sensors = data.get('sensors', [])
readings = data.get('readings', [])
# Créer un dictionnaire des sensors par index
sensor_map = {}
for sensor in sensors:
idx = sensor.get('entryIndex', 0)
sensor_map[idx] = {
'name': sensor.get('sensorNameUser', sensor.get('sensorNameOriginal', 'Unknown')),
'value': '',
'id': '',
'level': 0,
'type': 'group',
'children': []
}
# Ajouter les readings aux sensors correspondants
for reading in readings:
sensor_idx = reading.get('sensorIndex', 0)
if sensor_idx not in sensor_map:
continue
label = reading.get('labelUser', reading.get('labelOriginal', 'Unknown'))
value = reading.get('value', 0)
unit = reading.get('unit', '')
# Générer un ID unique
sensor_id = self._generate_sensor_id(sensor_idx, reading.get('readingId', 0), label)
# Formater la valeur
if isinstance(value, float):
if value == int(value):
formatted_value = f"{int(value)} {unit}".strip()
else:
formatted_value = f"{value:.2f} {unit}".strip()
else:
formatted_value = f"{value} {unit}".strip()
reading_entry = {
'name': label,
'value': formatted_value,
'id': sensor_id,
'level': 1,
'type': self.get_sensor_type(unit, label),
'children': []
}
sensor_map[sensor_idx]['children'].append(reading_entry)
# Retourner seulement les sensors qui ont des readings
return [s for s in sensor_map.values() if s['children']]
def parse_sensors(self, data: Dict) -> List[Dict]:
"""Parse les données et retourne une liste plate de capteurs"""
sensors_list = []
if not data:
return sensors_list
sensors = data.get('sensors', [])
readings = data.get('readings', [])
# Créer un dictionnaire des sensors par index
sensor_names = {}
for sensor in sensors:
idx = sensor.get('entryIndex', 0)
sensor_names[idx] = sensor.get('sensorNameUser', sensor.get('sensorNameOriginal', 'Unknown'))
# Parser chaque reading
for reading in readings:
sensor_idx = reading.get('sensorIndex', 0)
hardware_name = sensor_names.get(sensor_idx, 'Unknown')
label = reading.get('labelUser', reading.get('labelOriginal', 'Unknown'))
value = reading.get('value', 0)
unit = reading.get('unit', '')
# Générer un ID unique
sensor_id = self._generate_sensor_id(sensor_idx, reading.get('readingId', 0), label)
# Formater la valeur
if isinstance(value, float):
if value == int(value):
formatted_value = f"{int(value)} {unit}".strip()
else:
formatted_value = f"{value:.2f} {unit}".strip()
else:
formatted_value = f"{value} {unit}".strip()
sensor_type = self.get_sensor_type(unit, label)
hardware_type = self._guess_hardware_type(hardware_name)
sensor = {
'id': sensor_id,
'name': label,
'value': formatted_value,
'raw_value': float(value) if value else 0.0,
'type': sensor_type,
'unit': unit,
'hardware': hardware_name,
'hardware_type': hardware_type,
'category': hardware_name,
'path': f"{hardware_name}/{label}",
}
sensors_list.append(sensor)
return sensors_list
def _generate_sensor_id(self, sensor_idx: int, reading_id: int, label: str) -> str:
"""Génère un ID unique pour un capteur"""
# Nettoyer le label pour l'ID
clean_label = label.replace(' ', '_').replace('[', '').replace(']', '')
clean_label = clean_label.replace('#', '').replace(':', '_').replace('/', '_')
return f"/hwinfo/{sensor_idx}_{reading_id}_{clean_label}"
def _guess_hardware_type(self, sensor_name: str) -> str:
"""Devine le type de hardware basé sur le nom du sensor"""
name_lower = sensor_name.lower()
if 'cpu' in name_lower or 'processor' in name_lower:
return 'CPU'
elif 'gpu' in name_lower or 'graphics' in name_lower or 'radeon' in name_lower or 'geforce' in name_lower:
return 'GPU'
elif 'memory' in name_lower or 'ram' in name_lower or 'dimm' in name_lower:
return 'RAM'
elif 'drive' in name_lower or 'disk' in name_lower or 'ssd' in name_lower or 'hdd' in name_lower or 's.m.a.r.t' in name_lower:
return 'Storage'
elif 'network' in name_lower or 'ethernet' in name_lower or 'wifi' in name_lower or 'réseau' in name_lower:
return 'Network'
elif 'system' in name_lower or 'système' in name_lower:
return 'System'
else:
return 'Other'
# === Gestion de l'historique ===
def update_history(self, sensors: List[Dict]):
"""Met à jour l'historique des capteurs pour les graphiques"""
for sensor in sensors:
sensor_id = sensor.get("id", "")
if not sensor_id:
continue
raw_value = sensor.get("raw_value", 0.0)
if sensor_id not in self.history:
self.history[sensor_id] = []
self.history[sensor_id].append(raw_value)
if len(self.history[sensor_id]) > self.max_history:
self.history[sensor_id] = self.history[sensor_id][-self.max_history:]
def get_history(self, sensor_id: str, count: int = 60) -> List[float]:
"""Récupère l'historique d'un capteur"""
if sensor_id not in self.history:
return []
return self.history[sensor_id][-count:]

View File

@@ -0,0 +1,266 @@
"""
Plugin pour LibreHardwareMonitor
Utilise l'API REST intégrée de LHM (port 8085 par défaut)
"""
import requests
from typing import Dict, List, Optional, Any
from .base import BasePlugin
class LibreHardwareMonitorPlugin(BasePlugin):
"""Plugin pour LibreHardwareMonitor"""
def __init__(self, config: dict):
super().__init__(config)
self.history = {}
self.max_history = 120 # 2 minutes à 1 sample/sec
def get_id(self) -> str:
return 'librehardwaremonitor'
def get_name(self) -> str:
return 'LibreHardwareMonitor'
def get_default_port(self) -> int:
return 8085
def test_connection(self) -> Dict[str, Any]:
"""Teste la connexion à LibreHardwareMonitor"""
try:
url = f"{self.get_base_url()}/data.json"
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
# Extraire la version si disponible
version = "Inconnue"
if data and "Text" in data:
version = data.get("Text", "LibreHardwareMonitor")
# Compter les capteurs
sensors = self.parse_sensors(data)
return {
'success': True,
'message': f'Connecté - {len(sensors)} capteurs détectés',
'version': version,
'sensor_count': len(sensors)
}
except requests.exceptions.ConnectionError:
return {
'success': False,
'message': f'Impossible de se connecter à {self.get_base_url()}. Vérifiez que LibreHardwareMonitor est lancé avec le serveur web activé.'
}
except requests.exceptions.Timeout:
return {
'success': False,
'message': 'Timeout - Le serveur ne répond pas'
}
except Exception as e:
return {
'success': False,
'message': f'Erreur: {str(e)}'
}
def get_data(self) -> Optional[Dict]:
"""Récupère les données JSON depuis LHM Remote Server"""
try:
url = f"{self.get_base_url()}/data.json"
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"Erreur LHM get_data: {e}")
return None
def get_hierarchy(self) -> List[dict]:
"""Récupère la hiérarchie des capteurs pour l'admin"""
data = self.get_data()
if not data:
return []
return self._build_hierarchy(data)
def _build_hierarchy(self, data: Dict) -> List[dict]:
"""Construit la hiérarchie des capteurs"""
if not data or "Children" not in data:
return []
def build_tree(node, level=0):
node_text = node.get("Text", "")
node_value = node.get("Value", "")
sensor_id = node.get("SensorId", "")
result = {
"name": node_text,
"value": node_value,
"id": sensor_id,
"level": level,
"type": self._guess_sensor_type(node_text, node_value) if node_value else "group",
"children": []
}
if "Children" in node and node["Children"]:
for child in node["Children"]:
result["children"].append(build_tree(child, level + 1))
return result
tree = []
for child in data.get("Children", []):
tree.append(build_tree(child))
return tree
def parse_sensors(self, data: Dict) -> List[Dict]:
"""Parse les données et retourne une liste plate de capteurs"""
sensors = []
if not data or "Children" not in data:
return sensors
def extract_sensors(node, parent_name="", hardware_name="", hardware_type=""):
node_text = node.get("Text", "")
current_path = f"{parent_name}/{node_text}" if parent_name else node_text
# Détecter le type de hardware au niveau 1
if not hardware_name and "Children" in node:
hw_type = self._guess_hardware_type(node_text)
hardware_name = node_text
hardware_type = hw_type
# Si le nœud a une valeur, c'est un capteur
if "Value" in node and node["Value"]:
sensor_id = node.get("SensorId", "")
if not sensor_id:
sensor_id = f"{current_path}".replace(" ", "-").lower()
value_str = node.get("Value", "")
raw_value = self._extract_numeric_value(value_str)
unit = self._extract_unit(value_str)
sensor_type = self._guess_sensor_type(node_text, value_str)
sensor = {
"id": sensor_id,
"name": node_text,
"value": value_str,
"raw_value": raw_value,
"type": sensor_type,
"unit": unit,
"hardware": hardware_name,
"hardware_type": hardware_type,
"category": parent_name.split("/")[0] if "/" in parent_name else parent_name,
"path": current_path,
"min": node.get("Min", ""),
"max": node.get("Max", ""),
}
sensors.append(sensor)
# Parcourir les enfants récursivement
if "Children" in node:
for child in node["Children"]:
extract_sensors(child, current_path, hardware_name, hardware_type)
for child in data.get("Children", []):
extract_sensors(child)
return sensors
def _guess_hardware_type(self, name: str) -> str:
"""Devine le type de hardware basé sur le nom"""
name_lower = name.lower()
if 'cpu' in name_lower or 'processor' in name_lower:
return 'CPU'
elif 'gpu' in name_lower or 'graphics' in name_lower or 'nvidia' in name_lower or 'amd' in name_lower or 'radeon' in name_lower or 'geforce' in name_lower:
return 'GPU'
elif 'memory' in name_lower or 'ram' in name_lower:
return 'RAM'
elif 'motherboard' in name_lower or 'mainboard' in name_lower:
return 'Motherboard'
elif 'storage' in name_lower or 'disk' in name_lower or 'ssd' in name_lower or 'hdd' in name_lower or 'nvme' in name_lower:
return 'Storage'
elif 'network' in name_lower or 'ethernet' in name_lower or 'wifi' in name_lower:
return 'Network'
elif 'battery' in name_lower:
return 'Battery'
else:
return 'Other'
def _guess_sensor_type(self, name: str, value: str) -> str:
"""Devine le type de capteur basé sur le nom et la valeur"""
name_lower = name.lower()
value_lower = value.lower()
if "°c" in value_lower or "temperature" in name_lower or "temp" in name_lower:
return "temperature"
elif "%" in value or "load" in name_lower or "usage" in name_lower:
return "percentage"
elif "rpm" in value_lower or "fan" in name_lower:
return "fan"
elif "mhz" in value_lower or "ghz" in value_lower or "clock" in name_lower:
return "frequency"
elif "w" in value_lower and "wh" not in value_lower:
return "power"
elif "v" in value_lower:
return "voltage"
elif "gb" in value_lower or "mb" in value_lower or "memory" in name_lower:
return "memory"
else:
return "generic"
def _extract_numeric_value(self, value_str: str) -> float:
"""Extrait la valeur numérique d'une chaîne"""
try:
# Enlever tous les caractères non numériques sauf le point et la virgule
clean = value_str.replace("°C", "").replace("%", "").replace("V", "").replace("W", "")
clean = clean.replace("MHz", "").replace("GHz", "").replace("GB", "").replace("MB", "")
clean = clean.replace("RPM", "").replace(" ", "").replace(",", ".").strip()
if clean:
return float(clean)
except (ValueError, AttributeError):
pass
return 0.0
def _extract_unit(self, value_str: str) -> str:
"""Extrait l'unité d'une valeur"""
value_str = value_str.strip()
units = ['°C', '°F', '%', 'MHz', 'GHz', 'GB', 'MB', 'KB', 'RPM', 'W', 'V', 'A', 'GB/s', 'MB/s']
for unit in units:
if unit in value_str:
return unit
return ''
# === Gestion de l'historique ===
def update_history(self, sensors: List[Dict]):
"""Met à jour l'historique des capteurs pour les graphiques"""
for sensor in sensors:
sensor_id = sensor.get("id", "")
if not sensor_id:
continue
raw_value = sensor.get("raw_value")
if raw_value is None:
raw_value = self._extract_numeric_value(sensor.get("value", ""))
if raw_value == 0.0 and "0" not in sensor.get("value", ""):
continue
if sensor_id not in self.history:
self.history[sensor_id] = []
self.history[sensor_id].append(raw_value)
if len(self.history[sensor_id]) > self.max_history:
self.history[sensor_id] = self.history[sensor_id][-self.max_history:]
def get_history(self, sensor_id: str, count: int = 60) -> List[float]:
"""Récupère l'historique d'un capteur"""
if sensor_id not in self.history:
return []
return self.history[sensor_id][-count:]

Some files were not shown because too many files have changed in this diff Show More