14 Commits

5 changed files with 1066 additions and 29 deletions

450
README.md
View File

@@ -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.

112
WARP.md Normal file
View File

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

99
config.yaml Normal file
View File

@@ -0,0 +1,99 @@
# SNMP2MQTT Configuration File
# This file contains the configuration for the SNMP to MQTT bridge
# MQTT Broker Configuration
mqtt:
broker: "192.168.10.202"
port: 1883
user: "snmp2mqtt"
password: "snmp_2_MQTT"
# 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: "192.168.10.2"
snmp_community: "public"
oids:
# Starlink VPN interface (interface index 12)
- name: "stln_vpn_in"
oid: ".1.3.6.1.2.1.2.2.1.10.12"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "stlon_vpn_out"
oid: ".1.3.6.1.2.1.2.2.1.16.12"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "stln_vpn_status"
oid: ".1.3.6.1.2.1.2.2.1.8.12"
type: "bool"
HA_device_class: "connectivity"
HA_platform: "binary_sensor"
# LAN Bridge interface (interface index 6)
- name: "lan_bridge_in"
oid: ".1.3.6.1.2.1.2.2.1.10.6"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "lan_bridge_out"
oid: ".1.3.6.1.2.1.2.2.1.16.6"
type: "int"
HA_device_class: "data_size"
HA_platform: "sensor"
HA_unit: "bit"
- name: "lan_bridge_status"
oid: ".1.3.6.1.2.1.2.2.1.8.6"
type: "bool"
HA_device_class: "connectivity"
HA_platform: "binary_sensor"
# Starlink interface (interface index 1)
- name: "starlink_in"
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"
# 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

12
requirements.txt Normal file
View File

@@ -0,0 +1,12 @@
# 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
paho-mqtt>=1.6.0
# YAML configuration file parsing
PyYAML>=6.0.0

422
snmp2mqtt.py Normal file → Executable file
View File

@@ -1,47 +1,413 @@
#!/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',
format='(%(levelname)s) [%(threadName)s] %(message)s',
level=logging.DEBUG
)
# 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, flags, rc):
if rc == 0:
print("Connected to MQTT Broker!")
else:
print("Failed to connect, return code {rc}")
client = mqtt_client.Client()
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}")
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]}")
logging.debug(f"{req['device_name']} {oid['name']} => {oid['type'](varBind[1])}")
if oid['type'] == bool:
if bool(varBind[1]):
data.update({oid["name"]: "ON"})
else:
data.update({oid["name"]: "OFF"})
else:
data.update({oid["name"]: oid["type"](varBind[1])})
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
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"
}
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 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()