initial commit
This commit is contained in:
59
plugins/__init__.py
Normal file
59
plugins/__init__.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Plugins pour PC Monitor
|
||||
from .base import BasePlugin
|
||||
from .librehardwaremonitor import LibreHardwareMonitorPlugin
|
||||
from .hwinfo import HWiNFOPlugin
|
||||
from .plexamp import PlexampPlugin
|
||||
|
||||
# Registry des plugins monitoring hardware
|
||||
PLUGINS = {
|
||||
'librehardwaremonitor': LibreHardwareMonitorPlugin,
|
||||
'hwinfo': HWiNFOPlugin
|
||||
}
|
||||
|
||||
# Registry des plugins média
|
||||
MEDIA_PLUGINS = {
|
||||
'plexamp': PlexampPlugin
|
||||
}
|
||||
|
||||
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_media_plugin(name: str, config: dict):
|
||||
"""Retourne une instance du plugin média demandé"""
|
||||
if name not in MEDIA_PLUGINS:
|
||||
raise ValueError(f"Plugin média inconnu: {name}")
|
||||
return MEDIA_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'
|
||||
}
|
||||
]
|
||||
|
||||
def get_available_media_plugins() -> list:
|
||||
"""Retourne la liste des plugins média disponibles"""
|
||||
return [
|
||||
{
|
||||
'id': 'plexamp',
|
||||
'name': 'Plexamp',
|
||||
'description': 'Lecteur Plex (via serveur Plex distant)',
|
||||
'default_port': 32400,
|
||||
'website': 'https://plexamp.com'
|
||||
}
|
||||
]
|
||||
BIN
plugins/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
plugins/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
plugins/__pycache__/base.cpython-313.pyc
Normal file
BIN
plugins/__pycache__/base.cpython-313.pyc
Normal file
Binary file not shown.
BIN
plugins/__pycache__/hwinfo.cpython-313.pyc
Normal file
BIN
plugins/__pycache__/hwinfo.cpython-313.pyc
Normal file
Binary file not shown.
BIN
plugins/__pycache__/librehardwaremonitor.cpython-313.pyc
Normal file
BIN
plugins/__pycache__/librehardwaremonitor.cpython-313.pyc
Normal file
Binary file not shown.
BIN
plugins/__pycache__/plexamp.cpython-313.pyc
Normal file
BIN
plugins/__pycache__/plexamp.cpython-313.pyc
Normal file
Binary file not shown.
151
plugins/base.py
Normal file
151
plugins/base.py
Normal 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'
|
||||
274
plugins/hwinfo.py
Normal file
274
plugins/hwinfo.py
Normal 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:]
|
||||
266
plugins/librehardwaremonitor.py
Normal file
266
plugins/librehardwaremonitor.py
Normal 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:]
|
||||
219
plugins/plexamp.py
Normal file
219
plugins/plexamp.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Plugin Plexamp - Récupère l'état de lecture depuis le serveur Plex
|
||||
Interroge /status/sessions sur le serveur Plex pour voir les sessions actives
|
||||
|
||||
Configuration:
|
||||
- host: IP/hostname du serveur Plex (ex: 192.168.1.235)
|
||||
- port: Port du serveur Plex (défaut: 32400)
|
||||
- token: Token d'authentification Plex
|
||||
"""
|
||||
import requests
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
|
||||
class PlexampPlugin:
|
||||
"""Plugin pour Plexamp via serveur Plex"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.host = config.get('host', '192.168.1.235')
|
||||
self.port = config.get('port', 32400)
|
||||
self.token = config.get('token', '')
|
||||
self.config = config
|
||||
|
||||
def get_id(self) -> str:
|
||||
return 'plexamp'
|
||||
|
||||
def get_name(self) -> str:
|
||||
return 'Plexamp'
|
||||
|
||||
def get_default_port(self) -> int:
|
||||
return 32400
|
||||
|
||||
def get_base_url(self) -> str:
|
||||
return f"http://{self.host}:{self.port}"
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Headers pour l'API Plex"""
|
||||
return {
|
||||
'X-Plex-Token': self.token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
def test_connection(self) -> Dict[str, Any]:
|
||||
"""Teste la connexion au serveur Plex"""
|
||||
if not self.token:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Token Plex non configuré. Ajoutez-le dans les paramètres.'
|
||||
}
|
||||
|
||||
try:
|
||||
url = f"{self.get_base_url()}/status/sessions"
|
||||
response = requests.get(url, headers=self._get_headers(), timeout=5)
|
||||
|
||||
if response.status_code == 401:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Token invalide ou expiré.'
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'Connecté au serveur Plex ({self.host}:{self.port})',
|
||||
'version': 'Plex Media Server'
|
||||
}
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Impossible de se connecter à {self.get_base_url()}. Vérifiez l\'adresse du serveur.'
|
||||
}
|
||||
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_now_playing(self) -> Optional[Dict[str, Any]]:
|
||||
"""Récupère les infos de lecture en cours depuis le serveur Plex"""
|
||||
if not self.token:
|
||||
return None
|
||||
|
||||
try:
|
||||
url = f"{self.get_base_url()}/status/sessions"
|
||||
response = requests.get(url, headers=self._get_headers(), timeout=5)
|
||||
response.raise_for_status()
|
||||
|
||||
# Parser le JSON
|
||||
data = response.json()
|
||||
|
||||
media_container = data.get('MediaContainer', {})
|
||||
|
||||
# Vérifier s'il y a des sessions actives
|
||||
if media_container.get('size', 0) == 0:
|
||||
return {
|
||||
'playing': False,
|
||||
'state': 'stopped'
|
||||
}
|
||||
|
||||
# Chercher une session audio (Plexamp)
|
||||
metadata_list = media_container.get('Metadata', [])
|
||||
|
||||
music_session = None
|
||||
for session in metadata_list:
|
||||
# Filtrer les sessions audio
|
||||
if session.get('type') == 'track':
|
||||
music_session = session
|
||||
break
|
||||
|
||||
if not music_session:
|
||||
return {
|
||||
'playing': False,
|
||||
'state': 'stopped'
|
||||
}
|
||||
|
||||
# Extraire les infos
|
||||
player = music_session.get('Player', {})
|
||||
state = player.get('state', 'stopped')
|
||||
|
||||
# Temps en millisecondes
|
||||
view_offset = music_session.get('viewOffset', 0)
|
||||
duration = music_session.get('duration', 0)
|
||||
|
||||
result = {
|
||||
'playing': state == 'playing',
|
||||
'state': state,
|
||||
'time': view_offset,
|
||||
'duration': duration,
|
||||
'progress': (view_offset / duration * 100) if duration > 0 else 0,
|
||||
'title': music_session.get('title', ''),
|
||||
'artist': music_session.get('grandparentTitle', ''),
|
||||
'album': music_session.get('parentTitle', ''),
|
||||
'year': music_session.get('parentYear', ''),
|
||||
'thumb': music_session.get('thumb', ''),
|
||||
'art': music_session.get('art', ''),
|
||||
'player_name': player.get('title', ''),
|
||||
'player_device': player.get('device', ''),
|
||||
'session_key': music_session.get('sessionKey', ''),
|
||||
'rating_key': music_session.get('ratingKey', ''),
|
||||
'machine_id': player.get('machineIdentifier', '')
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur Plexamp get_now_playing: {e}")
|
||||
return None
|
||||
|
||||
def get_artwork_url(self, thumb_path: str) -> str:
|
||||
"""Construit l'URL complète pour l'artwork"""
|
||||
if not thumb_path:
|
||||
return ''
|
||||
return f"{self.get_base_url()}{thumb_path}?X-Plex-Token={self.token}"
|
||||
|
||||
# === Contrôles de lecture ===
|
||||
|
||||
def play(self) -> bool:
|
||||
"""Reprend la lecture"""
|
||||
return self._send_command('play')
|
||||
|
||||
def pause(self) -> bool:
|
||||
"""Met en pause"""
|
||||
return self._send_command('pause')
|
||||
|
||||
def play_pause(self) -> bool:
|
||||
"""Toggle play/pause"""
|
||||
now_playing = self.get_now_playing()
|
||||
if now_playing and now_playing.get('playing'):
|
||||
return self.pause()
|
||||
else:
|
||||
return self.play()
|
||||
|
||||
def next_track(self) -> bool:
|
||||
"""Piste suivante"""
|
||||
return self._send_command('skipNext')
|
||||
|
||||
def prev_track(self) -> bool:
|
||||
"""Piste précédente"""
|
||||
return self._send_command('skipPrevious')
|
||||
|
||||
def stop(self) -> bool:
|
||||
"""Arrête la lecture"""
|
||||
return self._send_command('stop')
|
||||
|
||||
def _send_command(self, command: str) -> bool:
|
||||
"""Envoie une commande au player via le serveur Plex"""
|
||||
if not self.token:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Récupérer la session active pour avoir le machineIdentifier
|
||||
now_playing = self.get_now_playing()
|
||||
if not now_playing or not now_playing.get('machine_id'):
|
||||
print("Pas de session active trouvée")
|
||||
return False
|
||||
|
||||
machine_id = now_playing['machine_id']
|
||||
|
||||
# Envoyer la commande au client
|
||||
url = f"{self.get_base_url()}/player/playback/{command}"
|
||||
params = {
|
||||
'commandID': 1,
|
||||
'type': 'music'
|
||||
}
|
||||
headers = self._get_headers()
|
||||
headers['X-Plex-Target-Client-Identifier'] = machine_id
|
||||
|
||||
response = requests.get(url, headers=headers, params=params, timeout=5)
|
||||
return response.status_code == 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur Plexamp command {command}: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user