Compare commits
9 Commits
config-fil
...
fix-snmp-e
Author | SHA1 | Date | |
---|---|---|---|
5b670c4708
|
|||
85a14f4fa0
|
|||
e4e79a34a9
|
|||
ffd86281ef
|
|||
e45c3c1f18
|
|||
7199432169
|
|||
4e2dec2441
|
|||
083cfcce1d
|
|||
96b0cd9b99
|
450
README.md
450
README.md
@@ -1,3 +1,451 @@
|
||||
# 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
|
||||
|
||||
## Architecture
|
||||
|
||||
### Composants principaux
|
||||
|
||||
- **Client SNMP** : Utilise `pysnmp.hlapi.asyncio.slim` pour la récupération asynchrone des données SNMP
|
||||
- **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
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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"
|
||||
|
||||
# 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`
|
||||
|
||||
### 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 DEBUG 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.
|
||||
|
12
config.yaml
12
config.yaml
@@ -82,18 +82,6 @@ devices:
|
||||
HA_device_class: "connectivity"
|
||||
HA_platform: "binary_sensor"
|
||||
|
||||
# Example of how to add another device:
|
||||
# another_device:
|
||||
# ip: "192.168.10.3"
|
||||
# snmp_community: "public"
|
||||
# oids:
|
||||
# - name: "cpu_usage"
|
||||
# oid: ".1.3.6.1.4.1.14988.1.1.3.14.0" # MikroTik CPU usage
|
||||
# type: "int"
|
||||
# HA_device_class: "power_factor"
|
||||
# HA_platform: "sensor"
|
||||
# HA_unit: "%"
|
||||
|
||||
# OID Configuration Reference:
|
||||
# - name: Unique identifier for this metric (used in MQTT topics and Home Assistant)
|
||||
# - oid: SNMP Object Identifier
|
||||
|
@@ -2,7 +2,8 @@
|
||||
# Install with: pip install -r requirements.txt
|
||||
|
||||
# SNMP library for asynchronous SNMP operations
|
||||
pysnmp>=6.0.0
|
||||
# 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
|
||||
paho-mqtt>=1.6.0
|
||||
|
347
snmp2mqtt.py
347
snmp2mqtt.py
@@ -1,7 +1,9 @@
|
||||
#!/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
|
||||
@@ -11,12 +13,99 @@ import yaml
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import signal
|
||||
import time
|
||||
|
||||
logging.basicConfig(
|
||||
format='(%(levelname)s) %(message)s',
|
||||
format='(%(levelname)s) [%(threadName)s] %(message)s',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
# Global shutdown flag
|
||||
shutdown_event = threading.Event()
|
||||
|
||||
|
||||
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"""
|
||||
@@ -103,7 +192,11 @@ def connect_mqtt(mqtt_config):
|
||||
|
||||
|
||||
def publish(topic, client, data, retain, qos):
|
||||
msg = json.dumps(data)
|
||||
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:
|
||||
@@ -113,25 +206,34 @@ def publish(topic, client, data, retain, qos):
|
||||
|
||||
|
||||
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"]:
|
||||
with Slim(1) as slim:
|
||||
errorIndication, errorStatus, errorIndex, varBinds = await slim.get(
|
||||
req["snmp_community"],
|
||||
req["ip"],
|
||||
161,
|
||||
ObjectType(ObjectIdentity(oid["oid"])),
|
||||
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"{req['device_name']} {oid['name']} => {oid['type'](varBind[1])}")
|
||||
@@ -142,101 +244,152 @@ async def get_snmp(req):
|
||||
data.update({oid["name"]: "OFF"})
|
||||
else:
|
||||
data.update({oid["name"]: oid["type"](varBind[1])})
|
||||
logging.debug(f"JSON : {json.dumps(data)}")
|
||||
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 ha_create_config(req):
|
||||
ha_config = {}
|
||||
device = {
|
||||
"ids": f"{req['device_name']}_{req['ip']}".replace(".", "_"),
|
||||
"name": req['device_name'],
|
||||
}
|
||||
origin = {
|
||||
"name": "snmp2mqtt"
|
||||
}
|
||||
ha_config.update({"dev": device, "o": origin})
|
||||
ha_config.update({"state_topic": f"SNMP/{req['device_name']}/state"})
|
||||
ha_config.update({"qos": 2})
|
||||
cmps = {}
|
||||
for oid in req['oids']:
|
||||
cmps.update(
|
||||
{
|
||||
f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_"):
|
||||
{
|
||||
"p": oid['HA_platform'],
|
||||
"device_class": oid['HA_device_class'],
|
||||
"value_template": f"{{{{ value_json.{oid['name']}}}}}",
|
||||
"unique_id": f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_"),
|
||||
"name": oid['name']
|
||||
}
|
||||
})
|
||||
if "HA_unit" in oid.keys():
|
||||
cmps.update(
|
||||
{f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_"):
|
||||
{"unit_of_measurement": oid['HA_unit']}})
|
||||
ha_config.update({"cmps": cmps})
|
||||
logging.debug(f"config : {json.dumps(ha_config)}")
|
||||
return ha_config
|
||||
|
||||
|
||||
def send_to_mqtt(device_name, device_config, mqtt_config, sleep_interval=2):
|
||||
"""Send SNMP data to MQTT for a single device"""
|
||||
# Create device request object
|
||||
req = {
|
||||
"device_name": device_name,
|
||||
"ip": device_config["ip"],
|
||||
"snmp_community": device_config["snmp_community"],
|
||||
"oids": device_config["oids"]
|
||||
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"
|
||||
}
|
||||
|
||||
config = ha_create_config(req)
|
||||
client = connect_mqtt(mqtt_config)
|
||||
client.loop_start()
|
||||
config_topic = f"homeassistant/device/{config['dev']['ids']}/config"
|
||||
state_topic = config['state_topic']
|
||||
|
||||
while True:
|
||||
try:
|
||||
publish(config_topic, client, config, True, 0)
|
||||
logging.info(f"{config_topic} -> {config}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error publishing config for {device_name}: {e}")
|
||||
pass
|
||||
try:
|
||||
state = asyncio.run(get_snmp(req))
|
||||
publish(state_topic, client, state, False, 0)
|
||||
logging.info(f"{state_topic} -> {state}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting SNMP data for {device_name}: {e}")
|
||||
pass
|
||||
sleep(sleep_interval)
|
||||
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 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
|
||||
|
||||
|
||||
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 from configuration"""
|
||||
"""Process multiple devices using threading"""
|
||||
mqtt_config = config['mqtt'].copy()
|
||||
mqtt_config['client_id'] = f"snmp-mqtt-{random.randint(0, 1000)}"
|
||||
|
||||
# Get sleep interval from config or use default
|
||||
sleep_interval = config.get('sleep_interval', 2)
|
||||
|
||||
if len(config['devices']) == 1:
|
||||
# Single device mode - run directly
|
||||
device_name = list(config['devices'].keys())[0]
|
||||
device_config = config['devices'][device_name]
|
||||
logging.info(f"Starting monitoring for single device: {device_name}")
|
||||
send_to_mqtt(device_name, device_config, mqtt_config, sleep_interval)
|
||||
else:
|
||||
# Multiple devices mode - would need threading/multiprocessing
|
||||
# For now, let's process the first device and warn about others
|
||||
logging.warning(f"Multiple devices detected ({len(config['devices'])}), but only processing the first one")
|
||||
logging.warning("Multi-device support will require threading implementation")
|
||||
# Setup signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
device_name = list(config['devices'].keys())[0]
|
||||
device_config = config['devices'][device_name]
|
||||
logging.info(f"Starting monitoring for device: {device_name}")
|
||||
send_to_mqtt(device_name, device_config, mqtt_config, sleep_interval)
|
||||
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():
|
||||
|
Reference in New Issue
Block a user