Compare commits
29 Commits
565edd6adc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
f44df8b1fe
|
|||
|
8e6d193d4c
|
|||
|
c350ab2203
|
|||
|
4516450a2e
|
|||
|
c0a8929084
|
|||
|
6c7248f08d
|
|||
|
7888999ec3
|
|||
|
ac476df7ce
|
|||
|
f48a9d1417
|
|||
|
b7331faba0
|
|||
|
3875f4501f
|
|||
|
7fec5c5049
|
|||
|
2309ef1deb
|
|||
|
75f8c6a637
|
|||
|
0d54632c52
|
|||
|
5b670c4708
|
|||
|
85a14f4fa0
|
|||
|
e4e79a34a9
|
|||
|
ffd86281ef
|
|||
|
e45c3c1f18
|
|||
|
7199432169
|
|||
|
4e2dec2441
|
|||
|
083cfcce1d
|
|||
|
96b0cd9b99
|
|||
|
ca4ad8ed06
|
|||
|
819ff2ed97
|
|||
|
0391f91809
|
|||
|
3796f500e7
|
|||
|
ec1dad8e8c
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -172,3 +172,4 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
|
||||
WARP.md
|
||||
|
||||
79
AGENTS.md
Normal file
79
AGENTS.md
Normal 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.
|
||||
481
README.md
481
README.md
@@ -1,3 +1,482 @@
|
||||
# snmp2mqtt
|
||||
|
||||
Script python pour relayer les infos snmp en mqtt
|
||||
Passerelle SNMP vers MQTT pour l'intégration Home Assistant. Ce script Python surveille les équipements réseau via SNMP et publie les données vers un broker MQTT pour une découverte automatique dans Home Assistant.
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- 🔧 **Configuration YAML flexible** : Tous les paramètres dans un fichier de configuration
|
||||
- 📡 **Support multi-équipements** : Surveillez plusieurs périphériques réseau
|
||||
- 🏠 **Intégration Home Assistant** : Découverte automatique des capteurs
|
||||
- ⚡ **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` (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
|
||||
|
||||
## Installation
|
||||
|
||||
### Prérequis
|
||||
|
||||
- Python 3.7 ou supérieur
|
||||
- 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
|
||||
# Cloner le repository
|
||||
git clone <url-du-repo>
|
||||
cd snmp2mqtt
|
||||
|
||||
# Créer un environnement virtuel
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Sur Linux/Mac
|
||||
# ou
|
||||
venv\Scripts\activate # Sur Windows
|
||||
|
||||
# Installer les dépendances
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Structure du fichier config.yaml
|
||||
|
||||
Le script utilise un fichier de configuration YAML avec la structure suivante :
|
||||
|
||||
```yaml
|
||||
# Configuration du broker MQTT
|
||||
mqtt:
|
||||
broker: "192.168.10.202" # Adresse IP du broker MQTT
|
||||
port: 1883 # Port du broker MQTT
|
||||
user: "snmp2mqtt" # Nom d'utilisateur MQTT
|
||||
password: "votre_mot_de_passe" # Mot de passe MQTT
|
||||
|
||||
# Intervalle entre les requêtes SNMP (optionnel, défaut: 2 secondes)
|
||||
sleep_interval: 2
|
||||
|
||||
# Configuration des équipements
|
||||
devices:
|
||||
nom_equipement:
|
||||
ip: "192.168.1.1" # Adresse IP de l'équipement
|
||||
snmp_community: "public" # Communauté SNMP
|
||||
oids: # Liste des OID à surveiller
|
||||
- name: "interface_in" # Nom unique du capteur
|
||||
oid: ".1.3.6.1.2.1.2.2.1.10.1" # OID SNMP
|
||||
type: "int" # Type de données (int, bool, str)
|
||||
HA_device_class: "data_size" # Classe d'équipement HA
|
||||
HA_platform: "sensor" # Plateforme HA
|
||||
HA_unit: "bit" # Unité (optionnel)
|
||||
```
|
||||
|
||||
### Paramètres de configuration détaillés
|
||||
|
||||
#### Section `mqtt`
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
|-----------|------|-------------|-------------|
|
||||
| `broker` | string | ✅ | Adresse IP ou nom d'hôte du broker MQTT |
|
||||
| `port` | integer | ✅ | Port du broker MQTT (généralement 1883) |
|
||||
| `user` | string | ✅ | Nom d'utilisateur pour l'authentification MQTT |
|
||||
| `password` | string | ✅ | Mot de passe pour l'authentification MQTT |
|
||||
|
||||
#### Section `devices`
|
||||
|
||||
Chaque équipement est défini par une clé (nom de l'équipement) et les paramètres suivants :
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
|-----------|------|-------------|-------------|
|
||||
| `ip` | string | ✅ | Adresse IP de l'équipement SNMP |
|
||||
| `snmp_community` | string | ✅ | Communauté SNMP (généralement "public") |
|
||||
| `oids` | liste | ✅ | Liste des OID SNMP à surveiller |
|
||||
|
||||
#### Configuration des OID
|
||||
|
||||
Chaque OID dans la liste `oids` doit contenir :
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
|-----------|------|-------------|-------------|
|
||||
| `name` | string | ✅ | Identifiant unique pour ce capteur |
|
||||
| `oid` | string | ✅ | Identifiant d'objet SNMP |
|
||||
| `type` | string | ✅ | Type de conversion Python ("int", "bool", "str") |
|
||||
| `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
|
||||
|
||||
- `data_size` : Pour les données de trafic réseau
|
||||
- `connectivity` : Pour le statut des interfaces
|
||||
- `power_factor` : Pour les pourcentages (CPU, utilisation)
|
||||
- `temperature` : Pour les températures
|
||||
- `signal_strength` : Pour la qualité du signal
|
||||
|
||||
### OID SNMP couramment utilisés
|
||||
|
||||
```yaml
|
||||
# Interfaces réseau (remplacez X par l'index de l'interface)
|
||||
- ".1.3.6.1.2.1.2.2.1.10.X" # Octets entrants sur l'interface X
|
||||
- ".1.3.6.1.2.1.2.2.1.16.X" # Octets sortants sur l'interface X
|
||||
- ".1.3.6.1.2.1.2.2.1.8.X" # Statut opérationnel (1=actif, 2=inactif)
|
||||
- ".1.3.6.1.2.1.2.2.1.2.X" # Description de l'interface
|
||||
|
||||
# MikroTik spécifique
|
||||
- ".1.3.6.1.4.1.14988.1.1.3.14.0" # Utilisation CPU
|
||||
- ".1.3.6.1.4.1.14988.1.1.6.1.0" # Température
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Lancement du script
|
||||
|
||||
```bash
|
||||
# Activer l'environnement virtuel
|
||||
source venv/bin/activate
|
||||
|
||||
# Lancer avec le fichier de configuration
|
||||
python snmp2mqtt.py --config config.yaml
|
||||
|
||||
# Ou avec la forme courte
|
||||
python snmp2mqtt.py -c config.yaml
|
||||
```
|
||||
|
||||
### Options en ligne de commande
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--config`, `-c` | Chemin vers le fichier de configuration YAML (obligatoire) |
|
||||
| `--help`, `-h` | Affiche l'aide |
|
||||
|
||||
## Intégration Home Assistant
|
||||
|
||||
### MQTT Discovery (Découverte automatique)
|
||||
|
||||
Le script implémente le protocole **MQTT Discovery** de Home Assistant pour une intégration transparente et automatique. Aucune configuration manuelle n'est nécessaire dans Home Assistant.
|
||||
|
||||
#### Fonctionnement de l'autodécouverte
|
||||
|
||||
1. **Au démarrage** : Publication des configurations de découverte
|
||||
2. **Pendant l'exécution** : Mise à jour continue des états des capteurs
|
||||
3. **Surveillance** : Gestion des statuts de disponibilité (online/offline)
|
||||
|
||||
### Topics MQTT générés
|
||||
|
||||
#### Topics de découverte (Discovery)
|
||||
Chaque capteur génère un topic de configuration individuel :
|
||||
```
|
||||
homeassistant/{platform}/{node_id}/{object_id}/config
|
||||
```
|
||||
|
||||
**Exemples** :
|
||||
```bash
|
||||
# Capteur de trafic réseau
|
||||
homeassistant/sensor/mikrotik_hex/mikrotik_hex_starlink_in/config
|
||||
|
||||
# Statut de connectivité
|
||||
homeassistant/binary_sensor/mikrotik_hex/mikrotik_hex_starlink_status/config
|
||||
```
|
||||
|
||||
#### Topics de données
|
||||
- **État** : `SNMP/{device_name}/state` - Données JSON des capteurs
|
||||
- **Disponibilité** : `SNMP/{device_name}/availability` - Statut online/offline
|
||||
|
||||
### Configuration automatique des capteurs
|
||||
|
||||
Chaque capteur est configuré avec :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mikrotik_hex starlink_in",
|
||||
"unique_id": "mikrotik_hex_192_168_10_2_starlink_in",
|
||||
"state_topic": "SNMP/mikrotik_hex/state",
|
||||
"value_template": "{{ value_json.starlink_in }}",
|
||||
"device_class": "data_size",
|
||||
"unit_of_measurement": "bit",
|
||||
"icon": "mdi:network",
|
||||
"device": {
|
||||
"identifiers": ["snmp2mqtt_mikrotik_hex_192_168_10_2"],
|
||||
"name": "mikrotik_hex",
|
||||
"model": "SNMP Device",
|
||||
"manufacturer": "Network Equipment"
|
||||
},
|
||||
"availability": {
|
||||
"topic": "SNMP/mikrotik_hex/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Découverte automatique dans Home Assistant
|
||||
|
||||
#### Regroupement par équipement
|
||||
Tous les capteurs d'un même équipement sont automatiquement regroupés sous une seule carte d'équipement :
|
||||
|
||||
- **Identifiant unique** basé sur `device_name` + `ip`
|
||||
- **Nom d'affichage** basé sur le nom de l'équipement
|
||||
- **Métadonnées** : modèle, fabricant, version du logiciel
|
||||
|
||||
#### Types de capteurs supportés
|
||||
|
||||
| Type HA | Platform | Description | Icône |
|
||||
|---------|----------|-------------|-------|
|
||||
| `sensor` | `sensor` | Valeurs numériques (trafic, CPU, etc.) | Selon device_class |
|
||||
| `binary_sensor` | `binary_sensor` | États binaires (online/offline, actif/inactif) | mdi:network-outline |
|
||||
|
||||
#### Classes d'équipements et icônes
|
||||
|
||||
| Device Class | Utilisation | Icône Auto | Unité Suggérée |
|
||||
|--------------|-------------|------------|----------------|
|
||||
| `data_size` | Trafic réseau, volumes de données | mdi:network | bit, byte, MB, GB |
|
||||
| `connectivity` | Statut des interfaces, connexions | mdi:network-outline | - |
|
||||
| `power_factor` | Pourcentages (CPU, mémoire) | mdi:gauge | % |
|
||||
| `temperature` | Températures d'équipements | mdi:thermometer | °C, °F |
|
||||
| `signal_strength` | Qualité des signaux | mdi:signal | dBm, % |
|
||||
|
||||
### Surveillance de disponibilité
|
||||
|
||||
#### Statuts de disponibilité
|
||||
- **Online** : Équipement accessible et données mises à jour
|
||||
- **Offline** : Équipement inaccessible ou erreurs SNMP
|
||||
|
||||
#### Mécanisme de heartbeat
|
||||
- Mise à jour du statut à chaque cycle de surveillance
|
||||
- Marquage offline automatique en cas d'erreur
|
||||
- Statut offline lors de l'arrêt du script
|
||||
|
||||
### Persistance et redémarrages
|
||||
|
||||
#### Configuration Discovery retenue
|
||||
- **Flag retain=true** sur les topics de configuration
|
||||
- **Redécouverte automatique** après redémarrage de Home Assistant
|
||||
- **Pas de perte de configuration** lors des redémarrages
|
||||
|
||||
#### Données d'état temps réel
|
||||
- **Flag retain=false** sur les données d'état
|
||||
- **Données fraîches uniquement** après redémarrage
|
||||
- **Historique préservé** par Home Assistant
|
||||
|
||||
### Intégration dans l'interface Home Assistant
|
||||
|
||||
Après démarrage du script, vous verrez automatiquement :
|
||||
|
||||
1. **Page Équipements** : Nouveaux équipements SNMP avec leurs capteurs
|
||||
2. **États et Historiques** : Données temps réel et graphiques
|
||||
3. **Cartes automatiques** : Ajout facile aux tableaux de bord
|
||||
4. **Notifications** : Alertes sur les changements d'état
|
||||
5. **Automations** : Utilisation des capteurs dans les règles
|
||||
|
||||
### Exemple d'équipement découvert
|
||||
|
||||
```
|
||||
📱 mikrotik_hex (SNMP Device)
|
||||
├── 📊 mikrotik_hex starlink_in (123.45 MB)
|
||||
├── 📊 mikrotik_hex starlink_out (67.89 MB)
|
||||
├── 🔌 mikrotik_hex starlink_status (Online)
|
||||
├── 📊 mikrotik_hex lan_bridge_in (234.56 MB)
|
||||
├── 📊 mikrotik_hex lan_bridge_out (78.90 MB)
|
||||
└── 🔌 mikrotik_hex lan_bridge_status (Online)
|
||||
|
||||
Statut: Online | Dernière mise à jour: il y a 2 secondes
|
||||
```
|
||||
|
||||
## Exemple de configuration complète
|
||||
|
||||
```yaml
|
||||
# Configuration MQTT
|
||||
mqtt:
|
||||
broker: "192.168.10.202"
|
||||
port: 1883
|
||||
user: "snmp2mqtt"
|
||||
password: "mon_mot_de_passe"
|
||||
|
||||
# Intervalle de surveillance
|
||||
sleep_interval: 5
|
||||
|
||||
# Équipements à surveiller
|
||||
devices:
|
||||
# Routeur MikroTik Hex
|
||||
mikrotik_hex:
|
||||
ip: "192.168.10.2"
|
||||
snmp_community: "public"
|
||||
oids:
|
||||
# Interface Starlink
|
||||
- name: "starlink_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"
|
||||
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"
|
||||
oid: ".1.3.6.1.2.1.2.2.1.8.1"
|
||||
type: "bool"
|
||||
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"
|
||||
snmp_community: "public"
|
||||
oids:
|
||||
- name: "port1_status"
|
||||
oid: ".1.3.6.1.2.1.2.2.1.8.1"
|
||||
type: "bool"
|
||||
HA_device_class: "connectivity"
|
||||
HA_platform: "binary_sensor"
|
||||
```
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Problèmes courants
|
||||
|
||||
1. **Erreur "Configuration file not found"**
|
||||
- Vérifiez le chemin vers le fichier config.yaml
|
||||
- Assurez-vous que le fichier existe et est lisible
|
||||
|
||||
2. **Erreur de connexion MQTT**
|
||||
- Vérifiez les paramètres du broker (IP, port, credentials)
|
||||
- Testez la connectivité réseau vers le broker
|
||||
|
||||
3. **Pas de données SNMP**
|
||||
- Vérifiez que l'équipement supporte SNMP
|
||||
- Testez avec `snmpwalk` : `snmpwalk -v2c -c public IP_EQUIPEMENT`
|
||||
- Vérifiez les OID utilisés
|
||||
|
||||
4. **Capteurs non découverts dans Home Assistant**
|
||||
- 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
|
||||
# Test SNMP manuel
|
||||
snmpwalk -v2c -c public 192.168.10.2 1.3.6.1.2.1.2.2.1.10
|
||||
|
||||
# Surveillance des messages MQTT
|
||||
mosquitto_sub -h 192.168.10.202 -u snmp2mqtt -P 'mot_de_passe' -t 'homeassistant/device/+/config'
|
||||
mosquitto_sub -h 192.168.10.202 -u snmp2mqtt -P 'mot_de_passe' -t 'SNMP/+/state'
|
||||
|
||||
# Test de connectivité
|
||||
ping 192.168.10.2
|
||||
ping 192.168.10.202
|
||||
```
|
||||
|
||||
## Support multi-équipements
|
||||
|
||||
Le script supporte nativement la surveillance simultanée de plusieurs équipements grâce à une architecture **multi-threading** :
|
||||
|
||||
### Fonctionnement
|
||||
|
||||
- **Thread indépendant** pour chaque équipement configuré
|
||||
- **Surveillance parallèle** : tous les équipements sont surveillés simultanément
|
||||
- **Isolation des erreurs** : la défaillance d'un équipement n'affecte pas les autres
|
||||
- **Clients MQTT séparés** : chaque thread utilise son propre client MQTT
|
||||
- **Arrêt gracieux** : tous les threads s'arrêtent proprement sur signal
|
||||
|
||||
### Avantages
|
||||
|
||||
- ⚡ **Performance optimale** : pas de blocage entre équipements
|
||||
- 🔄 **Traitement parallèle** : requêtes SNMP simultanées
|
||||
- 🛡️ **Robustesse** : isolation des défaillances
|
||||
- 📊 **Scalabilité** : facilement extensible à des dizaines d'équipements
|
||||
- 🔧 **Maintenance** : logs clairement identifiés par équipement
|
||||
|
||||
### Configuration multi-équipements
|
||||
|
||||
```yaml
|
||||
devices:
|
||||
routeur_principal:
|
||||
ip: "192.168.10.1"
|
||||
snmp_community: "public"
|
||||
oids:
|
||||
# ... configuration OID ...
|
||||
|
||||
switch_bureau:
|
||||
ip: "192.168.10.5"
|
||||
snmp_community: "public"
|
||||
oids:
|
||||
# ... configuration OID ...
|
||||
|
||||
point_acces_wifi:
|
||||
ip: "192.168.10.10"
|
||||
snmp_community: "private"
|
||||
oids:
|
||||
# ... configuration OID ...
|
||||
```
|
||||
|
||||
### Logs multi-threading
|
||||
|
||||
Chaque thread est clairement identifié dans les logs :
|
||||
|
||||
```
|
||||
(INFO) [Device-routeur_principal] Starting monitoring thread
|
||||
(INFO) [Device-switch_bureau] MQTT client connected
|
||||
(DEBUG) [Device-point_acces_wifi] Published state to SNMP/point_acces_wifi/state
|
||||
```
|
||||
|
||||
### Gestion des ressources
|
||||
|
||||
- **Clients MQTT uniques** : ID client basé sur le nom de l'équipement
|
||||
- **Topics séparés** : chaque équipement a ses propres topics MQTT
|
||||
- **Discovery HA indépendante** : configuration Home Assistant par équipement
|
||||
- **Disponibilité individuelle** : statut online/offline par équipement
|
||||
|
||||
## Logs et debugging
|
||||
|
||||
Le script utilise le module `logging` de Python avec le niveau INFO par défaut. Les logs incluent :
|
||||
|
||||
- Chargement de la configuration
|
||||
- Connexions MQTT
|
||||
- Requêtes SNMP et leurs résultats
|
||||
- Publication des messages MQTT
|
||||
- Erreurs et avertissements
|
||||
|
||||
## Licence
|
||||
|
||||
Ce projet est distribué sous licence libre. Consultez le fichier LICENSE pour plus de détails.
|
||||
|
||||
39
SYSTEMD.md
Normal file
39
SYSTEMD.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Service systemd pour snmp2mqtt
|
||||
|
||||
## Installation
|
||||
|
||||
Copiez le fichier dans `/etc/systemd/system/` :
|
||||
|
||||
```bash
|
||||
sudo cp snmp2mqtt.service /etc/systemd/system/
|
||||
sudo cp snmp-discover.service /etc/systemd/system/
|
||||
```
|
||||
|
||||
Créez un utilisateur dédié :
|
||||
|
||||
```bash
|
||||
sudo useradd -r -s /bin/false snmp2mqtt
|
||||
sudo chown -R snmp2mqtt:snmp2mqtt /home/snmp2mqtt/snmp2mqtt
|
||||
```
|
||||
|
||||
Rechargez systemd et activez le service :
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable snmp2mqtt
|
||||
sudo systemctl start snmp2mqtt
|
||||
```
|
||||
|
||||
## Journalisation
|
||||
|
||||
```bash
|
||||
journalctl -u snmp2mqtt -f
|
||||
```
|
||||
|
||||
## Pour snmp-discover
|
||||
|
||||
Le service `snmp-discover.service` est de type `oneshot` et ne doit pas être activé automatiquement. Il est destiné à être lancé manuellement pour configurer les appareils :
|
||||
|
||||
```bash
|
||||
sudo systemctl start snmp-discover
|
||||
```
|
||||
68
config.yaml
Normal file
68
config.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
# SNMP2MQTT Configuration File
|
||||
# This file contains the configuration for the SNMP to MQTT bridge
|
||||
|
||||
# MQTT Broker Configuration
|
||||
mqtt:
|
||||
broker: "IP or FQDN"
|
||||
port: 1883
|
||||
user: "USER"
|
||||
password: "PASSWORD"
|
||||
|
||||
# Optional: Sleep interval between SNMP polls (default: 2 seconds)
|
||||
sleep_interval: 2
|
||||
|
||||
# Device Configurations
|
||||
# You can define multiple devices here. Each device will be monitored independently.
|
||||
devices:
|
||||
# Device name (used for MQTT topics and Home Assistant device identification)
|
||||
mikrotik_hex:
|
||||
ip: "IP"
|
||||
snmp_community: "public"
|
||||
oids:
|
||||
# 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: "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: "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
|
||||
# - type: Python type for value conversion ("int", "bool", "str")
|
||||
# - HA_device_class: Home Assistant device class for proper icon/categorization
|
||||
# Common classes: data_size, connectivity, power_factor, temperature, etc.
|
||||
# - HA_platform: Home Assistant platform type ("sensor", "binary_sensor")
|
||||
# - HA_unit: (optional) Unit of measurement for the sensor
|
||||
# Common units: "bit", "byte", "%", "°C", "°F", etc.
|
||||
|
||||
# Common SNMP OIDs for network interfaces:
|
||||
# - .1.3.6.1.2.1.2.2.1.10.X = Incoming bytes on interface X
|
||||
# - .1.3.6.1.2.1.2.2.1.16.X = Outgoing bytes on interface X
|
||||
# - .1.3.6.1.2.1.2.2.1.8.X = Interface operational status (1=up, 2=down)
|
||||
# - .1.3.6.1.2.1.2.2.1.2.X = Interface description
|
||||
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# SNMP2MQTT Python Dependencies
|
||||
# Install with: pip install -r requirements.txt
|
||||
|
||||
# SNMP library for asynchronous SNMP operations
|
||||
# Note: pysnmp 7.x uses a new API structure (no more Slim class)
|
||||
pysnmp>=7.0.0
|
||||
|
||||
# MQTT client library for connecting to MQTT brokers
|
||||
# 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
557
snmp-discover.py
Normal 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()
|
||||
15
snmp-discover.service
Normal file
15
snmp-discover.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=SNMP Discovery Tool for snmp2mqtt
|
||||
Documentation=https://git.antoineve.me/AntoineVe/snmp2mqtt
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=snmp2mqtt
|
||||
Group=snmp2mqtt
|
||||
WorkingDirectory=/home/snmp2mqtt/snmp2mqtt
|
||||
ExecStart=/home/snmp2mqtt/snmp2mqtt/.venv/bin/python /home/snmp2mqtt/snmp2mqtt/snmp-discover.py --config /home/snmp2mqtt/snmp2mqtt/config.yaml
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
499
snmp2mqtt.py
Normal file → Executable file
499
snmp2mqtt.py
Normal file → Executable file
@@ -1,47 +1,488 @@
|
||||
#!/bin/env python3
|
||||
import asyncio
|
||||
from pysnmp.hlapi.asyncio.slim import Slim
|
||||
from pysnmp.smi.rfc1902 import ObjectIdentity, ObjectType
|
||||
from pysnmp.hlapi.asyncio import (
|
||||
get_cmd, CommunityData, UdpTransportTarget, ContextData,
|
||||
SnmpEngine, ObjectIdentity, ObjectType
|
||||
)
|
||||
import logging
|
||||
|
||||
import random
|
||||
from paho.mqtt import client as mqtt_client
|
||||
import json
|
||||
from time import sleep
|
||||
import yaml
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import signal
|
||||
import time
|
||||
|
||||
logging.basicConfig(
|
||||
format='(%(levelname)s) %(message)s',
|
||||
level=logging.DEBUG
|
||||
format='(%(levelname)s) [%(threadName)s] %(message)s',
|
||||
level=logging.INFO
|
||||
)
|
||||
|
||||
# Global shutdown flag
|
||||
shutdown_event = threading.Event()
|
||||
|
||||
async def run(req):
|
||||
for oid in req["oids"]:
|
||||
with Slim(1) as slim:
|
||||
errorIndication, errorStatus, errorIndex, varBinds = await slim.get(
|
||||
req["snmp_community"],
|
||||
req["ip"],
|
||||
161,
|
||||
ObjectType(ObjectIdentity(req["oids"][oid])),
|
||||
)
|
||||
|
||||
class DeviceMonitorThread(threading.Thread):
|
||||
"""Thread class for monitoring a single device"""
|
||||
|
||||
def __init__(self, device_name, device_config, mqtt_config, sleep_interval=2):
|
||||
super().__init__(name=f"Device-{device_name}")
|
||||
self.device_name = device_name
|
||||
self.device_config = device_config
|
||||
self.mqtt_config = mqtt_config.copy()
|
||||
self.sleep_interval = sleep_interval
|
||||
self.daemon = True # Dies when main thread dies
|
||||
|
||||
# Create unique client ID for this device
|
||||
self.mqtt_config['client_id'] = f"snmp-mqtt-{device_name}-{random.randint(0, 1000)}"
|
||||
|
||||
# Create device request object
|
||||
self.req = {
|
||||
"device_name": device_name,
|
||||
"ip": device_config["ip"],
|
||||
"snmp_community": device_config["snmp_community"],
|
||||
"oids": device_config["oids"]
|
||||
}
|
||||
|
||||
def run(self):
|
||||
"""Main thread execution"""
|
||||
logging.info(f"Starting monitoring thread for device: {self.device_name} ({self.device_config['ip']})")
|
||||
|
||||
try:
|
||||
# Setup MQTT connection
|
||||
client = connect_mqtt(self.mqtt_config)
|
||||
client.loop_start()
|
||||
|
||||
state_topic = f"SNMP/{self.device_name}/state"
|
||||
availability_topic = f"SNMP/{self.device_name}/availability"
|
||||
|
||||
logging.info(f"[{self.device_name}] MQTT client connected")
|
||||
|
||||
# Publish Home Assistant autodiscovery configuration (only once on startup)
|
||||
publish_ha_autodiscovery_config(client, self.req)
|
||||
|
||||
# Mark device as available
|
||||
publish(availability_topic, client, "online", True, 1)
|
||||
|
||||
logging.info(f"[{self.device_name}] Starting monitoring loop")
|
||||
|
||||
# Main monitoring loop
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
# Get SNMP data and publish state
|
||||
state = asyncio.run(get_snmp(self.req))
|
||||
publish(state_topic, client, state, False, 0)
|
||||
logging.debug(f"[{self.device_name}] Published state to {state_topic}: {json.dumps(state)}")
|
||||
|
||||
# Update availability (heartbeat)
|
||||
publish(availability_topic, client, "online", False, 1)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{self.device_name}] Error in monitoring loop: {e}")
|
||||
# Mark as offline on error
|
||||
publish(availability_topic, client, "offline", False, 1)
|
||||
|
||||
# Wait for next iteration or shutdown signal
|
||||
shutdown_event.wait(timeout=self.sleep_interval)
|
||||
|
||||
# Cleanup - mark device as offline
|
||||
publish(availability_topic, client, "offline", True, 1)
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
logging.info(f"[{self.device_name}] Monitoring thread stopped gracefully")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{self.device_name}] Fatal error in monitoring thread: {e}")
|
||||
# Try to mark as offline on fatal error
|
||||
try:
|
||||
publish(availability_topic, client, "offline", True, 1)
|
||||
client.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.info(f"[{self.device_name}] Thread {self.name} finished")
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments"""
|
||||
parser = argparse.ArgumentParser(description='SNMP to MQTT bridge for Home Assistant')
|
||||
parser.add_argument('--config', '-c', required=True,
|
||||
help='Path to YAML configuration file')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
"""Load and validate YAML configuration file"""
|
||||
if not os.path.exists(config_path):
|
||||
logging.error(f"Configuration file not found: {config_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
except yaml.YAMLError as e:
|
||||
logging.error(f"Error parsing YAML configuration: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading configuration file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate required configuration sections
|
||||
if 'mqtt' not in config:
|
||||
logging.error("Missing 'mqtt' section in configuration")
|
||||
sys.exit(1)
|
||||
|
||||
if 'devices' not in config:
|
||||
logging.error("Missing 'devices' section in configuration")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate MQTT configuration
|
||||
required_mqtt_fields = ['broker', 'port', 'user', 'password']
|
||||
for field in required_mqtt_fields:
|
||||
if field not in config['mqtt']:
|
||||
logging.error(f"Missing required MQTT field: {field}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate device configurations
|
||||
for device_name, device_config in config['devices'].items():
|
||||
required_device_fields = ['ip', 'snmp_community', 'oids']
|
||||
for field in required_device_fields:
|
||||
if field not in device_config:
|
||||
logging.error(f"Missing required field '{field}' in device '{device_name}'")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate OID configurations
|
||||
for oid in device_config['oids']:
|
||||
required_oid_fields = ['name', 'oid', 'type', 'HA_device_class', 'HA_platform']
|
||||
for field in required_oid_fields:
|
||||
if field not in oid:
|
||||
logging.error(f"Missing required OID field '{field}' in device '{device_name}'")
|
||||
sys.exit(1)
|
||||
|
||||
# Convert type string to actual Python type
|
||||
if oid['type'] == 'int':
|
||||
oid['type'] = int
|
||||
elif oid['type'] == 'bool':
|
||||
oid['type'] = bool
|
||||
elif oid['type'] == 'str':
|
||||
oid['type'] = str
|
||||
else:
|
||||
logging.error(f"Unsupported type '{oid['type']}' for OID '{oid['name']}' in device '{device_name}'")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(f"Configuration loaded successfully from {config_path}")
|
||||
return config
|
||||
|
||||
|
||||
def connect_mqtt(mqtt_config):
|
||||
def on_connect(client, userdata, connect_flags, reason_code, properties):
|
||||
if reason_code == 0:
|
||||
logging.info("Connected to MQTT Broker!")
|
||||
else:
|
||||
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'])
|
||||
return client
|
||||
|
||||
|
||||
def publish(topic, client, data, retain, qos):
|
||||
if isinstance(data, str):
|
||||
msg = data
|
||||
else:
|
||||
msg = json.dumps(data)
|
||||
|
||||
result = client.publish(topic=topic, payload=msg, qos=qos, retain=bool(retain))
|
||||
status = result[0]
|
||||
if status == 0:
|
||||
logging.debug(f"Send `{msg}` to topic `{topic}`")
|
||||
else:
|
||||
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 = {}
|
||||
|
||||
# Create SNMP engine and transport target
|
||||
snmpEngine = SnmpEngine()
|
||||
authData = CommunityData(req["snmp_community"])
|
||||
transportTarget = await UdpTransportTarget.create((req["ip"], 161))
|
||||
contextData = ContextData()
|
||||
|
||||
for oid in req["oids"]:
|
||||
try:
|
||||
# Perform async SNMP GET operation
|
||||
errorIndication, errorStatus, errorIndex, varBinds = await get_cmd(
|
||||
snmpEngine,
|
||||
authData,
|
||||
transportTarget,
|
||||
contextData,
|
||||
ObjectType(ObjectIdentity(oid["oid"]))
|
||||
)
|
||||
|
||||
if errorIndication:
|
||||
logging.error(errorIndication)
|
||||
logging.error(f"{req['device_name']} SNMP error indication: {errorIndication}")
|
||||
continue
|
||||
elif errorStatus:
|
||||
logging.error(
|
||||
"{} at {}".format(
|
||||
errorStatus.prettyPrint(),
|
||||
errorIndex and varBinds[int(errorIndex) - 1][0] or "?",
|
||||
)
|
||||
f"{req['device_name']} SNMP error status: {errorStatus.prettyPrint()} at {errorIndex and varBinds[int(errorIndex) - 1][0] or '?'}"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
for varBind in varBinds:
|
||||
logging.debug(f"SNMP/{req['mqtt_topic']}/{oid} => {varBind[1]}")
|
||||
raw_value = varBind[1]
|
||||
|
||||
# Handle different SNMP value types
|
||||
if oid['type'] == int and hasattr(raw_value, '__bytes__'):
|
||||
# Convert OctetString/bytes to int
|
||||
# For Mikrotik devices, memory values might be in the first 4 bytes
|
||||
raw_bytes = bytes(raw_value)
|
||||
# Try to interpret as integer, handling different byte orders
|
||||
try:
|
||||
if len(raw_bytes) >= 4:
|
||||
# Try little-endian first (common for Mikrotik)
|
||||
value = int.from_bytes(raw_bytes[:4], byteorder='little', signed=False)
|
||||
else:
|
||||
value = int.from_bytes(raw_bytes, byteorder='big', signed=False)
|
||||
except (ValueError, TypeError):
|
||||
value = raw_value
|
||||
else:
|
||||
value = raw_value
|
||||
|
||||
logging.debug(f"{req['device_name']} {oid['name']} => {oid['type'](value)}")
|
||||
|
||||
# Cast to the right type
|
||||
value = oid['type'](value)
|
||||
|
||||
# Apply operation if defined
|
||||
if 'operation' in oid:
|
||||
value = apply_operation(value, oid['operation'])
|
||||
|
||||
req = {
|
||||
"mqtt_topic": "mikrotik_hex",
|
||||
"ip": "192.168.10.2",
|
||||
"snmp_community": "public",
|
||||
"oids": {
|
||||
"stln_In": "1.3.6.1.2.1.2.2.1.10.12",
|
||||
"stln_Out": "1.3.6.1.2.1.2.2.1.16.12"
|
||||
}
|
||||
if oid['type'] == bool:
|
||||
if bool(value):
|
||||
data.update({oid["name"]: "ON"})
|
||||
else:
|
||||
data.update({oid["name"]: "OFF"})
|
||||
else:
|
||||
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
|
||||
|
||||
logging.debug(f"{req['device_name']} JSON : {json.dumps(data)}")
|
||||
return data
|
||||
|
||||
|
||||
def create_ha_device_info(req):
|
||||
"""Create device information for Home Assistant MQTT Discovery"""
|
||||
return {
|
||||
"identifiers": [f"snmp2mqtt_{req['device_name']}_{req['ip']}".replace(".", "_")],
|
||||
"name": req['device_name'],
|
||||
"model": "SNMP Device",
|
||||
"manufacturer": "Network Equipment",
|
||||
"via_device": "snmp2mqtt"
|
||||
}
|
||||
|
||||
|
||||
def create_ha_sensor_config(req, oid):
|
||||
"""Create Home Assistant MQTT Discovery configuration for a single sensor"""
|
||||
device_info = create_ha_device_info(req)
|
||||
sensor_id = f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_")
|
||||
|
||||
config = {
|
||||
"name": f"{req['device_name']} {oid['name']}",
|
||||
"unique_id": sensor_id,
|
||||
"state_topic": f"SNMP/{req['device_name']}/state",
|
||||
"value_template": f"{{{{ value_json.{oid['name']} }}}}",
|
||||
"device": device_info,
|
||||
"origin": {
|
||||
"name": "snmp2mqtt",
|
||||
"sw_version": "1.0.0",
|
||||
"support_url": "https://git.antoineve.me/AntoineVe/snmp2mqtt"
|
||||
}
|
||||
}
|
||||
|
||||
# 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:
|
||||
config['unit_of_measurement'] = oid['HA_unit']
|
||||
|
||||
# Add icon based on device class
|
||||
icon_mapping = {
|
||||
'data_size': 'mdi:network',
|
||||
'connectivity': 'mdi:network-outline',
|
||||
'power_factor': 'mdi:gauge',
|
||||
'temperature': 'mdi:thermometer',
|
||||
'signal_strength': 'mdi:signal'
|
||||
}
|
||||
if 'HA_device_class' in oid and oid['HA_device_class'] in icon_mapping:
|
||||
config['icon'] = icon_mapping[oid['HA_device_class']]
|
||||
|
||||
# Add availability topic
|
||||
config['availability'] = {
|
||||
"topic": f"SNMP/{req['device_name']}/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
asyncio.run(run(req))
|
||||
|
||||
def get_ha_discovery_topic(req, oid):
|
||||
"""Get the correct Home Assistant MQTT Discovery topic for a sensor"""
|
||||
platform = oid['HA_platform'] # 'sensor' or 'binary_sensor'
|
||||
node_id = req['device_name']
|
||||
object_id = f"{req['device_name']}_{oid['name']}".replace(".", "_")
|
||||
|
||||
# Format: homeassistant/<platform>/<node_id>/<object_id>/config
|
||||
return f"homeassistant/{platform}/{node_id}/{object_id}/config"
|
||||
|
||||
|
||||
def publish_ha_autodiscovery_config(client, req):
|
||||
"""Publish Home Assistant MQTT Discovery configuration for all sensors of a device"""
|
||||
logging.info(f"[{req['device_name']}] Publishing Home Assistant autodiscovery configuration")
|
||||
|
||||
# Publish availability as online
|
||||
availability_topic = f"SNMP/{req['device_name']}/availability"
|
||||
publish(availability_topic, client, "online", True, 1)
|
||||
|
||||
# Publish discovery configuration for each OID/sensor
|
||||
for oid in req['oids']:
|
||||
config = create_ha_sensor_config(req, oid)
|
||||
topic = get_ha_discovery_topic(req, oid)
|
||||
|
||||
# Publish with retain=True so HA discovers it after restarts
|
||||
publish(topic, client, config, True, 1)
|
||||
logging.info(f"[{req['device_name']}] Published discovery config for {oid['name']} to {topic}")
|
||||
|
||||
# Small delay to avoid overwhelming the broker
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Handle shutdown signals gracefully"""
|
||||
logging.info(f"Received signal {signum}, initiating graceful shutdown...")
|
||||
shutdown_event.set()
|
||||
|
||||
|
||||
def process_devices(config):
|
||||
"""Process multiple devices using threading"""
|
||||
mqtt_config = config['mqtt'].copy()
|
||||
sleep_interval = config.get('sleep_interval', 2)
|
||||
|
||||
# Setup signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
device_threads = []
|
||||
|
||||
try:
|
||||
logging.info(f"Starting monitoring for {len(config['devices'])} device(s)")
|
||||
|
||||
# Create and start a thread for each device
|
||||
for device_name, device_config in config['devices'].items():
|
||||
thread = DeviceMonitorThread(
|
||||
device_name=device_name,
|
||||
device_config=device_config,
|
||||
mqtt_config=mqtt_config,
|
||||
sleep_interval=sleep_interval
|
||||
)
|
||||
device_threads.append(thread)
|
||||
thread.start()
|
||||
logging.info(f"Started thread for device: {device_name}")
|
||||
|
||||
# Wait for all threads to complete or shutdown signal
|
||||
while any(thread.is_alive() for thread in device_threads) and not shutdown_event.is_set():
|
||||
time.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in process_devices: {e}")
|
||||
shutdown_event.set()
|
||||
|
||||
# Wait for all threads to finish
|
||||
logging.info("Waiting for all monitoring threads to finish...")
|
||||
for thread in device_threads:
|
||||
if thread.is_alive():
|
||||
thread.join(timeout=5.0) # Wait max 5 seconds per thread
|
||||
if thread.is_alive():
|
||||
logging.warning(f"Thread {thread.name} did not stop gracefully")
|
||||
|
||||
logging.info("All monitoring threads have finished")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
args = parse_arguments()
|
||||
config = load_config(args.config)
|
||||
|
||||
logging.info("Starting snmp2mqtt bridge...")
|
||||
logging.info(f"Configured devices: {list(config['devices'].keys())}")
|
||||
|
||||
try:
|
||||
process_devices(config)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Shutdown requested by user")
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
19
snmp2mqtt.service
Normal file
19
snmp2mqtt.service
Normal file
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=SNMP to MQTT bridge for Home Assistant
|
||||
Documentation=https://git.antoineve.me/AntoineVe/snmp2mqtt
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=snmp2mqtt
|
||||
Group=snmp2mqtt
|
||||
WorkingDirectory=/home/snmp2mqtt/snmp2mqtt
|
||||
ExecStart=/home/snmp2mqtt/snmp2mqtt/.venv/bin/python /home/snmp2mqtt/snmp2mqtt/snmp2mqtt.py --config /home/snmp2mqtt/snmp2mqtt/config.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user