5 Commits

4 changed files with 461 additions and 14 deletions

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

111
config.yaml Normal file
View File

@@ -0,0 +1,111 @@
# 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"
# 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
# - 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

11
requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
# SNMP2MQTT Python Dependencies
# Install with: pip install -r requirements.txt
# SNMP library for asynchronous SNMP operations
pysnmp>=6.0.0
# MQTT client library for connecting to MQTT brokers
paho-mqtt>=1.6.0
# YAML configuration file parsing
PyYAML>=6.0.0

237
snmp2mqtt.py Normal file → Executable file
View File

@@ -3,7 +3,14 @@ import asyncio
from pysnmp.hlapi.asyncio.slim import Slim from pysnmp.hlapi.asyncio.slim import Slim
from pysnmp.smi.rfc1902 import ObjectIdentity, ObjectType from pysnmp.smi.rfc1902 import ObjectIdentity, ObjectType
import logging 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
logging.basicConfig( logging.basicConfig(
format='(%(levelname)s) %(message)s', format='(%(levelname)s) %(message)s',
@@ -11,14 +18,109 @@ logging.basicConfig(
) )
async def run(req): 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):
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):
data = {}
for oid in req["oids"]: for oid in req["oids"]:
with Slim(1) as slim: with Slim(1) as slim:
errorIndication, errorStatus, errorIndex, varBinds = await slim.get( errorIndication, errorStatus, errorIndex, varBinds = await slim.get(
req["snmp_community"], req["snmp_community"],
req["ip"], req["ip"],
161, 161,
ObjectType(ObjectIdentity(req["oids"][oid])), ObjectType(ObjectIdentity(oid["oid"])),
) )
if errorIndication: if errorIndication:
@@ -32,16 +134,127 @@ async def run(req):
) )
else: else:
for varBind in varBinds: 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])})
logging.debug(f"JSON : {json.dumps(data)}")
return data
req = {
"mqtt_topic": "mikrotik_hex", def ha_create_config(req):
"ip": "192.168.10.2", ha_config = {}
"snmp_community": "public", device = {
"oids": { "ids": f"{req['device_name']}_{req['ip']}".replace(".", "_"),
"stln_In": "1.3.6.1.2.1.2.2.1.10.12", "name": req['device_name'],
"stln_Out": "1.3.6.1.2.1.2.2.1.16.12"
} }
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"]
} }
asyncio.run(run(req)) 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 process_devices(config):
"""Process multiple devices from configuration"""
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")
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)
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()