13 Commits

Author SHA1 Message Date
c350ab2203 feat: Ajoute snmp-discover.py pour la découverte semi-guidée des OIDs 2026-06-09 16:45:08 +02:00
4516450a2e Update AGENTS.md 2026-05-24 11:27:38 +02:00
c0a8929084 Fix: Gérer les OID invalides avec un avertissement
Plutôt que de générer une erreur lorsqu'un OID SNMP renvoie une valeur vide ou non-convertible, le système émet désormais un simple avertissement.

Cela évite de polluer les logs d'erreurs pour des cas non-bloquants, comme un port débranché qui renvoie une chaîne vide au lieu d'un entier.
2026-02-15 21:12:15 +01:00
6c7248f08d Mise à jour de la documentation (humaine + IA) 2025-11-06 17:08:37 +01:00
7888999ec3 feat: Ajoute la transformation de valeur par opération mathématique
Cette modification introduit la possibilité d'appliquer une opération mathématique simple (addition, soustraction, multiplication, division) sur les valeurs SNMP récupérées.

L'utilisateur peut désormais définir une clé 'operation' dans la configuration d'un OID (par exemple, 'value / 1000') pour normaliser les données avant leur envoi à MQTT. La documentation dans `config.yaml` a été mise à jour avec un exemple.
2025-11-06 17:05:20 +01:00
ac476df7ce fix: les capteurs 'data_size' ont l'attribut 'total_increasing' 2025-09-09 22:33:37 +02:00
f48a9d1417 Aide au codage 2025-09-09 22:32:57 +02:00
b7331faba0 logging INFO par défaut 2025-08-24 16:57:21 +02:00
3875f4501f Config example 2025-08-24 16:47:23 +02:00
7fec5c5049 DEBUG c'est quand même très verbeux -> passage à INFO:x 2025-08-24 16:39:08 +02:00
2309ef1deb Mise à jour avec la nouvelle API (V2) de Paho MQTT 2025-08-24 16:37:25 +02:00
75f8c6a637 Remove WARP.md from repo 2025-08-24 16:27:18 +02:00
0d54632c52 Mise à jour en lien avec les changements dans PySNMP 2025-08-24 16:25:35 +02:00
8 changed files with 755 additions and 173 deletions

1
.gitignore vendored
View File

@@ -172,3 +172,4 @@ cython_debug/
#.idea/
WARP.md

79
AGENTS.md Normal file
View File

@@ -0,0 +1,79 @@
# Fichier d'aide pour Gemini
Ce document fournit un résumé concis du projet `snmp2mqtt` pour aider au développement et à la maintenance assistés par l'IA.
## 1. Objectif du Projet
Le projet `snmp2mqtt` est une passerelle écrite en Python qui a pour but de :
1. **Interroger** des équipements réseau (routeurs, switchs, etc.) via le protocole **SNMP**.
2. **Récupérer** des métriques spécifiques (trafic, statut des ports, etc.) définies par des OIDs.
3. **Publier** ces données sur un broker **MQTT**.
4. **S'intégrer automatiquement** avec **Home Assistant** grâce au mécanisme de "MQTT Discovery", permettant de créer des capteurs sans configuration manuelle côté Home Assistant.
## 2. Architecture et Technologies
- **Langage** : Python 3
- **Dépendances principales** (`requirements.txt`) :
- `pysnmp>=7.0.0` : Pour la communication SNMP asynchrone.
- `paho-mqtt>=2.0.0` : Pour la communication avec le broker MQTT.
- `PyYAML>=6.0.0` : Pour le parsing du fichier de configuration.
- **Configuration** : Un unique fichier `config.yaml` centralise tous les paramètres (MQTT, appareils, OIDs).
- **Exécution** : Le script utilise le multi-threading. Un thread est démarré pour chaque appareil défini dans la configuration, ce qui permet une surveillance parallèle et isolée.
## 3. Structure du Code (`snmp2mqtt.py`)
Le script principal est organisé de la manière suivante :
1. **`main()`** : Point d'entrée. Il parse les arguments (`--config`), charge la configuration et appelle `process_devices()`.
2. **`process_devices(config)`** :
- Orchestre le lancement des threads.
- Crée et démarre une instance de `DeviceMonitorThread` pour chaque appareil.
- Gère l'arrêt propre (`graceful shutdown`) en attendant que tous les threads se terminent.
3. **`DeviceMonitorThread(threading.Thread)`** :
- Classe qui encapsule la logique de surveillance pour un seul appareil.
- `run()` : méthode principale du thread.
- Établit la connexion MQTT.
- Publie la configuration de découverte automatique pour Home Assistant (une seule fois au démarrage).
- Entre dans une boucle infinie qui :
- Appelle `get_snmp()` pour récupérer les données.
- Publie l'état des capteurs et le statut de disponibilité (`online`/`offline`) sur MQTT.
- Attend un intervalle (`sleep_interval`) avant la prochaine interrogation.
4. **`get_snmp(req)`** :
- Fonction `async` qui utilise `pysnmp` pour exécuter les requêtes `GET` SNMP pour tous les OIDs d'un appareil.
- Traite la clé optionnelle `operation` pour appliquer une transformation mathématique.
- Retourne un dictionnaire contenant les valeurs formatées.
5. **Fonctions de configuration et MQTT** :
- `load_config()` : Charge et valide le fichier `config.yaml`.
- `connect_mqtt()` : Initialise le client MQTT.
- `publish()` : Wrapper pour publier les messages MQTT.
- `publish_ha_autodiscovery_config()` : Construit et publie les messages de configuration pour Home Assistant MQTT Discovery.
- `apply_operation(value, operation_str)` : Fonction d'aide qui applique de manière sécurisée une opération mathématique simple à une valeur.
## 4. Flux de Données
```
[Appareil SNMP] <--- (Requête SNMP GET) --- [snmp2mqtt.py / Thread]
|
| (Réponse SNMP)
v
[snmp2mqtt.py / Thread] --- (Publication MQTT) ---> [Broker MQTT]
|
| (MQTT Discovery & State)
v
[Home Assistant]
```
## 5. Comment développer
- **Environnement** :
1. Créer un environnement virtuel : `python3 -m venv .venv`
2. Activer l'environnement : `source .venv/bin/activate`
3. Installer les dépendances : `pip install -r requirements.txt`
- **Configuration** :
- Copier et modifier `config.yaml` pour pointer vers un broker MQTT de test et un appareil SNMP accessible.
- **Lancement** :
- `python snmp2mqtt.py --config config.yaml`
- **Points clés à modifier** :
- Pour ajouter une nouvelle fonctionnalité à un capteur Home Assistant, modifier `create_ha_sensor_config()`.
- Pour changer la logique de récupération SNMP, modifier `get_snmp()`.
- Pour ajouter de nouveaux paramètres de configuration, mettre à jour `load_config()` pour la validation.

View File

@@ -10,12 +10,13 @@ Passerelle SNMP vers MQTT pour l'intégration Home Assistant. Ce script Python s
-**SNMP asynchrone** : Requêtes SNMP non-bloquantes pour de meilleures performances
- 🔄 **Surveillance en temps réel** : Mise à jour continue des métriques réseau
- 📊 **Métriques réseau** : Trafic entrant/sortant et statut des interfaces
- 🔢 **Transformation de données** : Appliquez des opérations mathématiques simples (division, multiplication...) pour normaliser les valeurs.
## Architecture
### Composants principaux
- **Client SNMP** : Utilise `pysnmp.hlapi.asyncio.slim` pour la récupération asynchrone des données SNMP
- **Client SNMP** : Utilise `pysnmp.hlapi.asyncio` (version 7.x) pour la récupération asynchrone des données SNMP avec `get_cmd`, `SnmpEngine` et `UdpTransportTarget`
- **Publisher MQTT** : Utilise `paho.mqtt.client` pour publier les données vers un broker MQTT
- **Intégration Home Assistant** : Génère la configuration de découverte automatique compatible avec Home Assistant MQTT Discovery
- **Traitement des données** : Convertit les valeurs des OID SNMP vers les types appropriés (int, bool) pour les capteurs Home Assistant
@@ -28,6 +29,16 @@ Passerelle SNMP vers MQTT pour l'intégration Home Assistant. Ce script Python s
- Accès réseau aux équipements SNMP à surveiller
- Broker MQTT accessible
### Dépendances principales
- **pysnmp >= 7.0.0** : Bibliothèque SNMP avec nouvelle API asynchrone
- **paho-mqtt >= 2.0.0** : Client MQTT pour la communication avec le broker
- **PyYAML >= 6.0.0** : Parsing des fichiers de configuration YAML
⚠️ **Notes importantes sur les versions** :
- **pysnmp 7.x** : Changements d'API incompatibles avec les versions 6.x et antérieures. L'ancienne classe `Slim` a été supprimée au profit de `get_cmd()` avec des objets `SnmpEngine`, `UdpTransportTarget`, etc.
- **paho-mqtt 2.x** : Nouvelle API de callbacks (VERSION2) qui remplace l'ancienne API deprecated (VERSION1). Les signatures des callbacks ont changé.
### Configuration de l'environnement
```bash
@@ -109,6 +120,7 @@ Chaque OID dans la liste `oids` doit contenir :
| `HA_device_class` | string | ✅ | Classe d'équipement Home Assistant |
| `HA_platform` | string | ✅ | Plateforme Home Assistant ("sensor", "binary_sensor") |
| `HA_unit` | string | ❌ | Unité de mesure pour le capteur |
| `operation` | string | ❌ | Opération mathématique à appliquer (ex: "value / 1000") |
### Classes d'équipements Home Assistant courantes
@@ -328,6 +340,15 @@ devices:
HA_device_class: "connectivity"
HA_platform: "binary_sensor"
# Exemple avec transformation de valeur (température en millidegrés -> degrés)
# - name: "temperature"
# oid: ".1.3.6.1.4.1.14988.1.1.6.1.0" # OID pour la température sur MikroTik
# type: "int"
# operation: "value / 1000"
# HA_device_class: "temperature"
# HA_platform: "sensor"
# HA_unit: "°C"
# Switch réseau
switch_bureau:
ip: "192.168.10.5"
@@ -361,6 +382,16 @@ devices:
- Vérifiez que MQTT Discovery est activé dans Home Assistant
- Surveillez les logs MQTT avec `mosquitto_sub`
5. **Erreurs liées à PySNMP**
- **"ModuleNotFoundError: No module named 'pysnmp.hlapi.asyncio.slim'"** : Vous utilisez une version pysnmp 6.x. Mettez à jour vers >= 7.0.0
- **"Please call .create() to construct UdpTransportTarget object"** : Erreur corrigée dans cette version, utilisez `pip install -r requirements.txt`
- **Erreurs d'importation SNMP** : Assurez-vous d'avoir pysnmp 7.x avec `pip show pysnmp`
6. **Erreurs liées à Paho MQTT**
- **"DeprecationWarning: Callback API version 1 is deprecated"** : Vous utilisez une version paho-mqtt < 2.0. Mettez à jour vers >= 2.0.0
- **Erreurs de callback MQTT** : La nouvelle API VERSION2 change la signature des callbacks (ex: `on_connect` reçoit maintenant 5 paramètres)
- **Vérification version** : `pip show paho-mqtt` pour confirmer la version installée
### Commandes de test utiles
```bash
@@ -438,7 +469,7 @@ Chaque thread est clairement identifié dans les logs :
## Logs et debugging
Le script utilise le module `logging` de Python avec le niveau DEBUG par défaut. Les logs incluent :
Le script utilise le module `logging` de Python avec le niveau INFO par défaut. Les logs incluent :
- Chargement de la configuration
- Connexions MQTT

112
WARP.md
View File

@@ -1,112 +0,0 @@
# WARP.md
This file provides guidance to WARP (warp.dev) when working with code in this repository.
## Project Overview
snmp2mqtt is a Python script that bridges SNMP network device monitoring with MQTT messaging for Home Assistant integration. It specifically monitors a MikroTik router (Hex) and publishes network interface statistics and status information to MQTT topics for Home Assistant discovery.
## Architecture
### Core Components
- **SNMP Client**: Uses `pysnmp.hlapi.asyncio.slim` for asynchronous SNMP data retrieval from network devices
- **MQTT Publisher**: Uses `paho.mqtt.client` to publish data to an MQTT broker
- **Home Assistant Integration**: Generates device discovery configuration compatible with Home Assistant MQTT Discovery
- **Data Processing**: Converts SNMP OID values to appropriate data types (int, bool) for Home Assistant sensors
### Key Functions
- `get_snmp(req)`: Asynchronously retrieves SNMP data from configured OIDs
- `connect_mqtt(mqtt_config)`: Establishes MQTT broker connection
- `publish(topic, client, data, retain, qos)`: Publishes JSON data to MQTT topics
- `ha_create_config(req)`: Generates Home Assistant device discovery configuration
- `send_to_mqtt()`: Main loop that continuously publishes config and state data
### Configuration Structure
The script uses two main configuration dictionaries:
- `req`: Defines the target device, SNMP community, and monitored OIDs with Home Assistant metadata
- `mqtt_config`: MQTT broker connection parameters
## Common Development Commands
### Environment Setup
```bash
# Activate virtual environment
source bin/activate
# Install dependencies (if needed)
pip install pysnmp paho-mqtt
# Check installed packages
pip list
```
### Running the Application
```bash
# Run the main script
python snmp2mqtt.py
# Run with Python 3 explicitly
python3 snmp2mqtt.py
# Run from virtual environment
./bin/python snmp2mqtt.py
```
### Development and Testing
```bash
# Test SNMP connectivity to device
# (Manual SNMP walk example)
snmpwalk -v2c -c public 192.168.10.2 1.3.6.1.2.1.2.2.1.10
# Monitor MQTT messages (if mosquitto-clients available)
mosquitto_sub -h 192.168.10.202 -u snmp2mqtt -P 'snmp_2_MQTT' -t 'homeassistant/device/+/config'
mosquitto_sub -h 192.168.10.202 -u snmp2mqtt -P 'snmp_2_MQTT' -t 'SNMP/+/state'
# Check network connectivity
ping 192.168.10.2
ping 192.168.10.202
```
## Configuration Notes
### Device Configuration
- Hardcoded to monitor MikroTik Hex router at IP `192.168.10.2`
- SNMP community: `public`
- Monitors interfaces: Starlink (index 1), LAN bridge (index 6), VPN (index 12)
### MQTT Configuration
- Broker: `192.168.10.202:1883`
- Credentials: `snmp2mqtt` / `snmp_2_MQTT`
- Config topic: `homeassistant/device/{device_id}/config`
- State topic: `SNMP/{device_name}/state`
### Monitored Metrics
For each interface:
- **Incoming bytes** (`oid: .1.3.6.1.2.1.2.2.1.10.X`) - Published as data_size sensor
- **Outgoing bytes** (`oid: .1.3.6.1.2.1.2.2.1.16.X`) - Published as data_size sensor
- **Interface status** (`oid: .1.3.6.1.2.1.2.2.1.8.X`) - Published as connectivity binary_sensor
## Customization Points
### Adding New Devices
1. Create new `req` configuration dictionary with device details
2. Update `mqtt_config` if different broker needed
3. Configure appropriate SNMP OIDs for the device type
### Adding New OIDs
Each OID entry requires:
- `name`: Unique identifier for Home Assistant
- `oid`: SNMP Object Identifier
- `type`: Python type conversion (int, bool)
- `HA_platform`: Home Assistant platform (sensor, binary_sensor)
- `HA_device_class`: Device class for proper Home Assistant categorization
- `HA_unit`: (optional) Unit of measurement
### Home Assistant Integration
The script automatically creates Home Assistant MQTT Discovery configuration with:
- Device identification and grouping
- Sensor types and units appropriate for network monitoring
- Value templates for JSON data extraction

View File

@@ -3,10 +3,10 @@
# MQTT Broker Configuration
mqtt:
broker: "192.168.10.202"
broker: "IP or FQDN"
port: 1883
user: "snmp2mqtt"
password: "snmp_2_MQTT"
user: "USER"
password: "PASSWORD"
# Optional: Sleep interval between SNMP polls (default: 2 seconds)
sleep_interval: 2
@@ -16,72 +16,41 @@ sleep_interval: 2
devices:
# Device name (used for MQTT topics and Home Assistant device identification)
mikrotik_hex:
ip: "192.168.10.2"
ip: "IP"
snmp_community: "public"
oids:
# Starlink VPN interface (interface index 12)
- name: "stln_vpn_in"
oid: ".1.3.6.1.2.1.2.2.1.10.12"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "stlon_vpn_out"
oid: ".1.3.6.1.2.1.2.2.1.16.12"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "stln_vpn_status"
oid: ".1.3.6.1.2.1.2.2.1.8.12"
type: "bool"
HA_device_class: "connectivity"
HA_platform: "binary_sensor"
# LAN Bridge interface (interface index 6)
- name: "lan_bridge_in"
oid: ".1.3.6.1.2.1.2.2.1.10.6"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "lan_bridge_out"
oid: ".1.3.6.1.2.1.2.2.1.16.6"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "lan_bridge_status"
oid: ".1.3.6.1.2.1.2.2.1.8.6"
type: "bool"
HA_device_class: "connectivity"
HA_platform: "binary_sensor"
# Starlink interface (interface index 1)
- name: "starlink_in"
# example interface index 1
- name: "if1_in"
oid: ".1.3.6.1.2.1.2.2.1.10.1"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "starlink_out"
- name: "if1_out"
oid: ".1.3.6.1.2.1.2.2.1.16.1"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "starlink_status"
- name: "if1_status"
oid: ".1.3.6.1.2.1.2.2.1.8.1"
type: "bool"
HA_device_class: "connectivity"
HA_platform: "binary_sensor"
# Example of a temperature sensor that returns the value in millidegrees.
# The 'operation' key allows performing a simple calculation.
# The placeholder 'value' will be replaced by the SNMP value.
# - name: "temperature"
# oid: ".1.3.6.1.4.1.XXXX.1.1.1.5.1.3.1" # Example OID
# type: "int"
# operation: "value / 1000"
# HA_device_class: "temperature"
# HA_platform: "sensor"
# HA_unit: "°C"
# OID Configuration Reference:
# - name: Unique identifier for this metric (used in MQTT topics and Home Assistant)
# - oid: SNMP Object Identifier

View File

@@ -6,7 +6,8 @@
pysnmp>=7.0.0
# MQTT client library for connecting to MQTT brokers
paho-mqtt>=1.6.0
# Note: paho-mqtt 2.x uses a new callback API (VERSION2) instead of the deprecated VERSION1
paho-mqtt>=2.0.0
# YAML configuration file parsing
PyYAML>=6.0.0

557
snmp-discover.py Normal file
View File

@@ -0,0 +1,557 @@
#!/usr/bin/env python3
"""
snmp-discover.py - Assistant de découverte SNMP semi-guidé pour snmp2mqtt.
Détecte automatiquement les OIDs disponibles sur un appareil MikroTik ou TP-Link Omada
et met à jour le fichier config.yaml.
"""
import asyncio
import yaml
import shutil
import os
import argparse
import sys
from datetime import datetime
from typing import List, Dict, Any, Optional, Tuple
from pysnmp.hlapi.asyncio import (
SnmpEngine,
CommunityData,
UdpTransportTarget,
ContextData,
ObjectIdentity,
ObjectType,
get_cmd,
walk_cmd,
)
# Catalogue des OIDs par type d'appareil
DEVICE_CATALOGS = {
"mikrotik": {
"manufacturer": "MikroTik",
"system_metrics": {
"cpu_usage": {
"oid": ".1.3.6.1.4.1.14988.1.1.3.14.0",
"name_template": "cpu_usage",
"type": "int",
"HA_device_class": "power_factor",
"HA_platform": "sensor",
"HA_unit": "%",
},
"temperature": {
"oid": ".1.3.6.1.4.1.14988.1.1.6.1.0",
"name_template": "temperature",
"type": "int",
"operation": "value / 1000",
"HA_device_class": "temperature",
"HA_platform": "sensor",
"HA_unit": "°C",
},
"memory_used": {
"oid": ".1.3.6.1.4.1.14988.1.1.4.2.0",
"name_template": "memory_used",
"type": "int",
"HA_device_class": "data_size",
"HA_platform": "sensor",
"HA_unit": "MB",
# Note: MikroTik memory OIDs can vary by version, assuming bytes here requires operation or custom handling.
# Standard HOST-RESOURCES-MIB is often used: hrMemorySize (.1.3.6.1.2.1.25.2.2.0)
# But MikroTik has proprietary ones. Let's adjust operation if needed.
# For now, we'll stick to a simple GET, user may need to tweak operation if values are weird.
"operation": "value / 1000000",
},
"memory_free": {
"oid": ".1.3.6.1.4.1.14988.1.1.4.3.0",
"name_template": "memory_free",
"type": "int",
"HA_device_class": "data_size",
"HA_platform": "sensor",
"HA_unit": "MB",
"operation": "value / 1000000",
},
},
"interfaces_root_oid": ".1.3.6.1.2.1.2.2.1.2",
},
"omada": {
"manufacturer": "TP-Link",
"system_metrics": {
"connected_clients": {
"oid": ".1.3.6.1.4.1.11863.10.1.3.0",
"name_template": "connected_clients",
"type": "int",
"HA_device_class": None,
"HA_platform": "sensor",
"HA_unit": "clients",
},
},
"interfaces_root_oid": ".1.3.6.1.2.1.2.2.1.2",
},
}
def parse_arguments():
parser = argparse.ArgumentParser(description='SNMP Discovery Tool for snmp2mqtt')
parser.add_argument('--config', '-c', default='config.yaml',
help='Path to YAML configuration file (default: config.yaml)')
return parser.parse_args()
async def snmp_get(ip, community, oid_str):
"""Récupère la valeur d'un OID spécifique via SNMP."""
snmpEngine = SnmpEngine()
try:
errorIndication, errorStatus, errorIndex, varBinds = await get_cmd(
snmpEngine,
CommunityData(community),
await UdpTransportTarget.create((ip, 161)),
ContextData(),
ObjectType(ObjectIdentity(oid_str))
)
snmpEngine.close_dispatcher()
if errorIndication:
return None, str(errorIndication)
elif errorStatus:
return None, f"{errorStatus.prettyPrint()}"
else:
for varBind in varBinds:
return str(varBind[1]), None
except Exception as e:
return None, str(e)
finally:
snmpEngine.close_dispatcher()
async def snmp_walk(ip, community, oid_prefix):
"""Effectue un SNMP WALK sur une branche OID."""
results = []
snmpEngine = SnmpEngine()
try:
async for errorIndication, errorStatus, errorIndex, varBinds in walk_cmd(
snmpEngine,
CommunityData(community),
await UdpTransportTarget.create((ip, 161)),
ContextData(),
ObjectType(ObjectIdentity(oid_prefix)),
lexicographicMode=True
):
if errorIndication or errorStatus:
break
for varBind in varBinds:
oid_str = str(varBind[0])
# Le test startswith doit gérer les formats avec/sans le point initial
# oid_prefix = ".1.3.6.1.2.1.2.2.1.2", oid_str = "1.3.6.1.2.1.2.2.1.2.1"
oid_str_normalized = oid_str.lstrip(".")
oid_prefix_normalized = oid_prefix.lstrip(".")
if oid_str_normalized.startswith(oid_prefix_normalized):
results.append((oid_str, str(varBind[1])))
except Exception as e:
print(f"Warning: SNMP walk error on {oid_prefix}: {e}")
finally:
snmpEngine.close_dispatcher()
return results
async def detect_device_type(ip, community):
"""Détecte le type d'équipement via sysDescr."""
# .1.3.6.1.2.1.1.1.0 = sysDescr
value, err = await snmp_get(ip, community, ".1.3.6.1.2.1.1.1.0")
if err:
return None, None, err
desc_lower = value.lower()
device_type = "unknown"
if "mikrotik" in desc_lower or "routeros" in desc_lower:
device_type = "mikrotik"
elif "eap" in desc_lower or "tp-link" in desc_lower or "omada" in desc_lower:
device_type = "omada"
return device_type, value, None
async def get_device_name(ip, community, fallback_type):
"""Récupère sysName pour nommer l'appareil, sinon utilise un fallback."""
# .1.3.6.1.2.1.1.5.0 = sysName
value, err = await snmp_get(ip, community, ".1.3.6.1.2.1.1.5.0")
if value and not err:
return value
# Fallback
prefix = fallback_type if fallback_type != "unknown" else "device"
sanitized_ip = ip.replace(".", "_")
# Check if name exists in config to avoid duplicates (simple check)
return f"{prefix}_{sanitized_ip}"
async def discover_interfaces(ip, community) -> List[Dict[str, Any]]:
"""Découvre les interfaces physiques via ifDescr."""
# ifDescr OID base
ifdescr_oid = ".1.3.6.1.2.1.2.2.1.2"
print(f" [+] Scanning interfaces...")
raw_ifs = await snmp_walk(ip, community, ifdescr_oid)
print(f" [DEBUG] Found {len(raw_ifs)} interfaces via walk")
interfaces = []
for oid_full, name in raw_ifs:
# Extraction de l'index (dernière partie de l'OID)
index = oid_full.split(".")[-1]
# Filtrage basique pour ne garder que les interfaces physiques
name_lower = name.lower()
if any(skip in name_lower for skip in ["loopback", "null", "dummy"]):
continue
# Détection des trucs un peu weird (parfois 'vlan', 'bridge' sont utiles, parfois non)
# On garde 'ether', 'sfp', 'wlan', 'eth', 'port', 'bridge', 'lan', 'wan'
if not any(k in name_lower for k in ["ether", "sfp", "wlan", "eth", "port", "bridge", "lan", "wan"]):
# Si on est sur un Omada ou switch, on pourrait avoir des noms génériques
# On reste permissif mais on exclut les trucs vides
if name.strip() == "":
continue
interface = {
"index": index,
"name": name,
"oids": []
}
# Construction des OID pour in, out, status
# in: .10.index, out: .16.index, status: .8.index
prefix = ".1.3.6.1.2.1.2.2.1"
interface["oids"].append({
"name": f"{name}_in",
"oid": f"{prefix}.10.{index}",
"type": "int",
"HA_device_class": "data_size",
"HA_platform": "sensor",
"HA_unit": "bit",
})
interface["oids"].append({
"name": f"{name}_out",
"oid": f"{prefix}.16.{index}",
"type": "int",
"HA_device_class": "data_size",
"HA_platform": "sensor",
"HA_unit": "bit",
})
interface["oids"].append({
"name": f"{name}_status",
"oid": f"{prefix}.8.{index}",
"type": "int", # On peut mettre bool ensuite si on veut map 1->ON
"HA_device_class": "connectivity",
"HA_platform": "binary_sensor",
})
interfaces.append(interface)
print(f" [DEBUG] Returning {len(interfaces)} interfaces")
return interfaces
async def discover_metrics(ip, community, device_type) -> List[Dict[str, Any]]:
"""Teste la disponibilité des OIDs systèmes spécifiques au fabricant."""
catalog = DEVICE_CATALOGS.get(device_type, {}).get("system_metrics", {})
available_metrics = []
if not catalog:
return available_metrics
print(f" [+] Scanning system metrics ({device_type})...")
for key, meta in catalog.items():
oid = meta.get("oid")
if oid:
val, err = await snmp_get(ip, community, oid)
if val and not err:
available_metrics.append({**meta, "key": key})
else:
print(f" - {key}: not available / error")
return available_metrics
def display_catalog(device_name, interfaces, system_metrics, device_type):
"""Affiche le menu interactif de sélection."""
print(f"\n" + "="*60)
print(f" Appareil détecté: {device_type.upper()}"
f"\n Nom: {device_name}")
print("="*60)
all_items = []
# 1. Interfaces
if interfaces:
print(f"\nInterfaces détectées ({len(interfaces)}):")
for idx, iface in enumerate(interfaces):
num = len(all_items) + 1
label = f" [{num}] {iface['name']} (In/Out/Status)"
print(label)
all_items.append({"type": "interface", "data": iface, "label": label})
# 2. Métriques système
if system_metrics:
prefix_map = {
"cpu": "CPU",
"temp": "Température",
"mem": "RAM"
}
print(f"\nMétriques Système:")
for idx, met in enumerate(system_metrics):
num = len(all_items) + 1
# Essayez de deviner un label propre
key_label = met['key'].replace('_', ' ').title()
unit = met.get('HA_unit', '')
op_label = ""
if met.get('operation'):
op_label = " (transformed)"
label = f" [{num}] {key_label} ({unit}){op_label}"
print(label)
all_items.append({"type": "metric", "data": met, "label": label})
if not all_items:
print("\nAucun OID intéressant trouvé.")
return [], []
print("\n" + "-"*40)
print("\nSélectionnez les métriques à surveiller :")
return all_items
def parse_selection(input_str: str, max_num: int) -> List[int]:
"""Parse la sélection utilisateur : '1,3,5-8', 'all', etc."""
indices = set()
if input_str.lower() == "all":
return list(range(1, max_num + 1))
elif input_str.lower() == "none":
return []
parts = input_str.split(",")
for part in parts:
part = part.strip()
if "-" in part:
try:
start, end = part.split("-", 1)
s = int(start)
e = int(end)
for n in range(s, e + 1):
if 1 <= n <= max_num:
indices.add(n)
except ValueError:
continue
else:
try:
n = int(part)
if 1 <= n <= max_num:
indices.add(n)
except ValueError:
continue
return sorted(list(indices))
def backup_config(config_path):
"""Crée une copie de backup du fichier config."""
# On suppose que le path est absolu ou relatif au cwd, on travaille avec le path tel quel
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
bak_path = f"{config_path}.bak.{timestamp}"
try:
# Créer un backup même si le fichier n'existe pas n'a pas de sens,
# mais s'il existe on le copie.
if os.path.exists(config_path):
shutil.copy2(config_path, bak_path)
print(f"[Backup] Sauvegarde créée: {bak_path}")
return True
else:
print(f"[Backup] Pas de fichier existant à sauvegarder, création du fichier.")
return True
except Exception as e:
print(f"[Erreur] Impossible de créer le backup: {e}")
return False
def build_device_oids(interface_data, metrics_data):
"""Construit la liste des objets OID pour le config."""
oids = []
# Ajout des interfaces (groupées)
for if_item in interface_data:
for oid in if_item["data"]["oids"]:
oids.append(oid)
# Ajout des métriques
for met_item in metrics_data:
oid_meta = met_item["data"]
clean_meta = {
"name": oid_meta.get("name_template", "metric"),
"oid": oid_meta["oid"],
"type": oid_meta["type"],
"HA_platform": oid_meta.get("HA_platform"),
}
if oid_meta.get("HA_device_class"):
clean_meta["HA_device_class"] = oid_meta["HA_device_class"]
if oid_meta.get("HA_unit"):
clean_meta["HA_unit"] = oid_meta["HA_unit"]
if "operation" in oid_meta:
clean_meta["operation"] = oid_meta["operation"]
oids.append(clean_meta)
return oids
def add_device_to_config(config_path, device_name, ip, community, device_oids):
"""Ajoute le device au fichier YAML."""
# Structure du nouveau device
new_device = {
"ip": ip,
"snmp_community": community,
"oids": device_oids
}
config = {}
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
content = f.read()
if content.strip():
config = yaml.safe_load(content) or {}
except yaml.YAMLError:
# Si le fichier est corrompu, on ne veut pas perdre le backup fait avant
print(f"[Erreur] Le fichier {config_path} semble invalide. Veuillez vérifier le backup.")
return False
# Assurer la structure de base
if 'mqtt' not in config:
config['mqtt'] = {
'broker': 'IP or FQDN',
'port': 1883,
'user': 'admin',
'password': 'password'
}
if 'devices' not in config:
config['devices'] = {}
# Insertion
config['devices'][device_name] = new_device
# Sauvegarde
try:
with open(config_path, 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
print(f"[-] Device '{device_name}' ajouté avec succès.")
return True
except Exception as e:
print(f"[Erreur] Echec de l'écriture du fichier config: {e}")
return False
async def process_device_loop(config_path):
"""Gère le loop d'ajout de devices."""
while True:
print("\n--- Découverte d'un nouvel appareil ---")
ip = input("Adresse IP de l'appareil : ").strip()
if not ip:
continue
community = input("Communauté SNMP (défaut: public) : ").strip() or "public"
# 1. Détection
print(f"[*] Connexion à {ip}...")
device_type, sysdescr, err = await detect_device_type(ip, community)
if err:
print(f"[!] Erreur SNMP: {err}")
retry = input("Réessayer ? (o/n) : ").lower()
if retry == 'y' or retry == 'o':
continue
else:
break
if device_type == "unknown":
print(f"[!] Type d'appareil inconnu. Description: {sysdescr}")
force_type = input("Forcer le type ? (mikrotik/omada/none) : ").lower()
if force_type in ["mikrotik", "omada"]:
device_type = force_type
else:
print("Annulé.")
break
print(f"[+] Appareil détecté: {device_type}")
# 2. Nom
device_name = await get_device_name(ip, community, device_type)
# Nettoyage du nom pour qu'il soit un key YAML safe
device_name = device_name.replace(" ", "_").lower()
# 3. Découverte Interfaces
interfaces = await discover_interfaces(ip, community)
# 4. Découverte Métriques
system_metrics = await discover_metrics(ip, community, device_type)
# 5. Display & Sélection
catalog_items = display_catalog(device_name, interfaces, system_metrics, device_type)
if not catalog_items:
choice = input("Continuer avec un autre appareil ? (o/n) : ")
if choice.lower() != 'o' and choice.lower() != 'y':
break
continue
max_idx = len(catalog_items)
selection_str = input(f"\nSélection (1-{max_idx}, ranges, 'all', 'none') : ")
selected_indices = parse_selection(selection_str, max_idx)
if not selected_indices:
print("Aucune sélection.")
choice = input("Continuer avec un autre appareil ? (o/n) : ")
if choice.lower() != 'o' and choice.lower() != 'y':
break
continue
# Filtrer les items sélectionnés
selected_interfaces = []
selected_metrics = []
for idx in selected_indices:
# Attention, indices start at 1
item = catalog_items[idx-1]
if item["type"] == "interface":
selected_interfaces.append(item)
else:
selected_metrics.append(item)
# Construction du device yaml
device_oids = build_device_oids(selected_interfaces, selected_metrics)
# 6. Backup & Save
print(f"\n--- Mise à jour de la configuration ---")
if backup_config(config_path):
success = add_device_to_config(config_path, device_name, ip, community, device_oids)
if success:
pass # Already printed success message
else:
print("[!] Veuillez restaurer manuellement depuis le backup si nécessaire.")
else:
print("[!] Annulé car backup impossible.")
# 7. Continuer ?
cont = input("\nAjouter un autre appareil ? (o/n) : ")
if cont.lower() != 'o' and cont.lower() != 'y':
break
def main():
args = parse_arguments()
asyncio.run(process_device_loop(args.config))
if __name__ == "__main__":
main()

View File

@@ -19,7 +19,7 @@ import time
logging.basicConfig(
format='(%(levelname)s) [%(threadName)s] %(message)s',
level=logging.DEBUG
level=logging.INFO
)
# Global shutdown flag
@@ -179,12 +179,14 @@ def load_config(config_path):
def connect_mqtt(mqtt_config):
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT Broker!")
def on_connect(client, userdata, connect_flags, reason_code, properties):
if reason_code == 0:
logging.info("Connected to MQTT Broker!")
else:
print("Failed to connect, return code {rc}")
client = mqtt_client.Client()
logging.error(f"Failed to connect to MQTT Broker, reason code: {reason_code}")
# Use the new callback API version 2
client = mqtt_client.Client(callback_api_version=mqtt_client.CallbackAPIVersion.VERSION2)
client.username_pw_set(mqtt_config["user"], mqtt_config["password"])
client.on_connect = on_connect
client.connect(mqtt_config['broker'], mqtt_config['port'])
@@ -205,6 +207,46 @@ def publish(topic, client, data, retain, qos):
logging.error(f"Failed to send message to topic {topic}")
def apply_operation(value, operation_str):
"""
Applies a simple mathematical operation to a value.
e.g., operation_str = "value / 1000"
"""
if 'value' not in operation_str:
logging.error(f"Invalid operation string: 'value' placeholder missing in '{operation_str}'")
return value
expression = operation_str.replace('value', str(value))
try:
parts = expression.split()
if len(parts) != 3:
logging.error(f"Invalid operation format: '{operation_str}'. Expected 'value <operator> <operand>'")
return value
val = float(parts[0])
operator = parts[1]
operand = float(parts[2])
if operator == '+':
return val + operand
elif operator == '-':
return val - operand
elif operator == '*':
return val * operand
elif operator == '/':
if operand == 0:
logging.warning(f"Attempted division by zero in operation: {operation_str}")
return value
return val / operand
else:
logging.error(f"Unsupported operator: '{operator}' in '{operation_str}'")
return value
except (ValueError, IndexError) as e:
logging.error(f"Could not parse operation string: '{operation_str}'. Error: {e}")
return value
async def get_snmp(req):
"""Asynchronously retrieve SNMP data from device using new pysnmp API"""
data = {}
@@ -237,13 +279,24 @@ async def get_snmp(req):
else:
for varBind in varBinds:
logging.debug(f"{req['device_name']} {oid['name']} => {oid['type'](varBind[1])}")
# Cast to the right type
value = oid['type'](varBind[1])
# Apply operation if defined
if 'operation' in oid:
value = apply_operation(value, oid['operation'])
if oid['type'] == bool:
if bool(varBind[1]):
if bool(value):
data.update({oid["name"]: "ON"})
else:
data.update({oid["name"]: "OFF"})
else:
data.update({oid["name"]: oid["type"](varBind[1])})
data.update({oid["name"]: value})
except ValueError as e:
logging.warning(f"{req['device_name']} OID {oid['oid']} ({oid['name']}) returned an invalid value: {e}")
continue
except Exception as e:
logging.error(f"{req['device_name']} Exception getting OID {oid['oid']}: {e}")
continue
@@ -284,6 +337,9 @@ def create_ha_sensor_config(req, oid):
# Add device class if specified
if 'HA_device_class' in oid:
config['device_class'] = oid['HA_device_class']
# Add state_class for total_increasing counters like data size
if oid['HA_device_class'] == 'data_size':
config['state_class'] = 'total_increasing'
# Add unit of measurement if specified
if 'HA_unit' in oid: