Files
modem-data-monitor/app.py
T
2026-08-30 04:48:55 +00:00

311 lines
9.9 KiB
Python

from flask import Flask, render_template, request, jsonify, redirect, url_for
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime
import subprocess
import re
import database as db
app = Flask(__name__)
# Modem configuration profiles
MODEM_PROFILES = {
'rv50': {
'oid_sent': '.1.3.6.1.4.1.20542.9.1.1.2.284.0',
'oid_recv': '.1.3.6.1.4.1.20542.9.1.1.2.283.0',
'type': 'counter64'
},
'bullet-lte': {
'oid_sent': '.1.3.6.1.4.1.21703.6030.9.4.3.0',
'oid_recv': '.1.3.6.1.4.1.21703.6030.9.4.1.0',
'type': 'displaystring'
}
}
# Store last readings for rate calculation
last_readings = {}
scheduler = BackgroundScheduler()
scheduler.start()
def format_bytes(bytes_value):
"""Convert bytes to human-readable format"""
if bytes_value is None or bytes_value == 0:
return "N/A", 0
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if abs(bytes_value) < 1024.0:
return f"{bytes_value:.2f} {unit}", bytes_value
bytes_value /= 1024.0
return f"{bytes_value:.2f} PB", bytes_value
def parse_snmp_value(value, data_type):
"""Parse SNMP response based on data type"""
if value is None:
return None
if data_type == 'counter64':
# Counter64: "Counter64: 1234567890"
match = re.search(r'Counter64:\s*(\d+)', value)
if match:
return int(match.group(1))
elif data_type == 'displaystring':
# DisplayString: "STRING: 1234567890" or just the number
match = re.search(r'STRING:\s*"?([^"]+)"?', value)
if match:
str_value = match.group(1).strip()
# Remove any non-digit characters (commas, spaces, etc.)
digits = re.sub(r'[^\d]', '', str_value)
if digits:
return int(digits)
# Fallback: try to find any number in the output
digits = re.sub(r'[^\d]', '', value)
if digits:
return int(digits)
return None
def snmp_get(ip, port, community, oid):
"""Execute snmpget command and return raw output"""
try:
cmd = [
'snmpget', '-v', '2c', '-c', community,
'-t', '5', '-r', '2',
f'{ip}:{port}', oid
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return result.stdout.strip()
return None
except Exception as e:
print(f"SNMP error: {e}")
return None
def poll_target(target_id, ip, port, community, modem_type):
"""Poll a target and store the reading"""
global last_readings
profile = MODEM_PROFILES.get(modem_type, MODEM_PROFILES['rv50'])
data_type = profile['type']
# Get raw SNMP responses
raw_sent = snmp_get(ip, port, community, profile['oid_sent'])
raw_recv = snmp_get(ip, port, community, profile['oid_recv'])
# Parse values based on data type
bytes_sent = parse_snmp_value(raw_sent, data_type)
bytes_recv = parse_snmp_value(raw_recv, data_type)
if bytes_sent is not None and bytes_recv is not None:
db.add_reading(target_id, bytes_sent, bytes_recv)
# Calculate rates (bytes per second)
rate_sent = 0
rate_recv = 0
if target_id in last_readings:
last_sent, last_recv, last_time = last_readings[target_id]
time_diff = (datetime.now() - last_time).total_seconds()
if time_diff > 0 and bytes_sent >= last_sent and bytes_recv >= last_recv:
rate_sent = (bytes_sent - last_sent) / time_diff
rate_recv = (bytes_recv - last_recv) / time_diff
last_readings[target_id] = (bytes_sent, bytes_recv, datetime.now())
return {
'bytes_sent': bytes_sent,
'bytes_recv': bytes_recv,
'rate_sent': rate_sent,
'rate_recv': rate_recv,
'success': True
}
return {'success': False, 'error': 'Failed to parse SNMP response'}
def scheduled_poll():
"""Background job to poll all enabled targets"""
targets = db.get_all_targets()
for target in targets:
if target['enabled']:
poll_target(
target['id'],
target['ip_address'],
target['port'],
target['community'],
target['modem_type']
)
@app.route('/')
def index():
targets = db.get_all_targets()
target_data = []
for target in targets:
latest = db.get_latest_reading(target['id'])
if latest:
sent_formatted, _ = format_bytes(latest['bytes_sent'])
recv_formatted, _ = format_bytes(latest['bytes_recv'])
# Calculate rate if we have previous reading
rate_sent = 0
rate_recv = 0
if target['id'] in last_readings:
last_sent, last_recv, last_time = last_readings[target['id']]
time_diff = (datetime.now() - last_time).total_seconds()
if time_diff > 0 and latest['bytes_sent'] >= last_sent:
rate_sent = (latest['bytes_sent'] - last_sent) / time_diff
rate_recv = (latest['bytes_recv'] - last_recv) / time_diff
rate_sent_fmt, _ = format_bytes(rate_sent)
rate_recv_fmt, _ = format_bytes(rate_recv)
else:
sent_formatted = recv_formatted = rate_sent_fmt = rate_recv_fmt = "N/A"
target_data.append({
**target,
'bytes_sent_fmt': sent_formatted,
'bytes_recv_fmt': recv_formatted,
'rate_sent_fmt': f"{rate_sent_fmt}/s",
'rate_recv_fmt': f"{rate_recv_fmt}/s",
'last_poll': latest['timestamp'] if latest else None
})
return render_template('index.html', targets=target_data)
@app.route('/add', methods=['POST'])
def add_target():
name = request.form.get('name', 'Unnamed')
ip_address = request.form.get('ip_address')
port = int(request.form.get('port', 161))
community = request.form.get('community', 'public')
period_minutes = int(request.form.get('period_minutes', 5))
modem_type = request.form.get('modem_type', 'rv50')
if ip_address:
target_id = db.add_target(name, ip_address, port, community, period_minutes, modem_type)
scheduler.add_job(
scheduled_poll,
'interval',
minutes=period_minutes,
id=f'target_{target_id}',
replace_existing=True
)
poll_target(target_id, ip_address, port, community, modem_type)
return redirect(url_for('index'))
@app.route('/edit/<int:target_id>', methods=['POST'])
def edit_target(target_id):
name = request.form.get('name', 'Unnamed')
ip_address = request.form.get('ip_address')
port = int(request.form.get('port', 161))
community = request.form.get('community', 'public')
period_minutes = int(request.form.get('period_minutes', 5))
enabled = 1 if request.form.get('enabled') else 0
modem_type = request.form.get('modem_type', 'rv50')
db.update_target(target_id, name, ip_address, port, community, period_minutes, enabled, modem_type)
if enabled:
scheduler.add_job(
scheduled_poll,
'interval',
minutes=period_minutes,
id=f'target_{target_id}',
replace_existing=True
)
else:
try:
scheduler.remove_job(f'target_{target_id}')
except:
pass
return redirect(url_for('index'))
@app.route('/delete/<int:target_id>', methods=['POST'])
def delete_target(target_id):
try:
scheduler.remove_job(f'target_{target_id}')
except:
pass
db.delete_target(target_id)
return redirect(url_for('index'))
@app.route('/poll/<int:target_id>', methods=['POST'])
def manual_poll(target_id):
target = db.get_target(target_id)
if target:
result = poll_target(
target['id'],
target['ip_address'],
target['port'],
target['community'],
target['modem_type']
)
return jsonify(result)
return jsonify({'success': False, 'error': 'Target not found'}), 404
@app.route('/graph/<int:target_id>')
def graph(target_id):
target = db.get_target(target_id)
hours = request.args.get('hours', 24, type=int)
readings = db.get_readings(target_id, hours)
labels = []
sent_data = []
recv_data = []
for r in readings:
labels.append(r['timestamp'])
sent_data.append(r['bytes_sent'])
recv_data.append(r['bytes_recv'])
return render_template('graph.html',
target=target,
labels=labels,
sent_data=sent_data,
recv_data=recv_data,
hours=hours)
@app.route('/api/readings/<int:target_id>')
def api_readings(target_id):
hours = request.args.get('hours', 24, type=int)
readings = db.get_readings(target_id, hours)
data = []
for r in readings:
data.append({
'timestamp': r['timestamp'],
'bytes_sent': r['bytes_sent'],
'bytes_recv': r['bytes_recv']
})
return jsonify(data)
if __name__ == '__main__':
db.init_db()
targets = db.get_all_targets()
for target in targets:
if target['enabled']:
scheduler.add_job(
scheduled_poll,
'interval',
minutes=target['period_minutes'],
id=f'target_{target["id"]}',
replace_existing=True
)
poll_target(
target['id'],
target['ip_address'],
target['port'],
target['community'],
target['modem_type']
)
app.run(debug=True, host='0.0.0.0', port=5500)