commit 295410075f4af8fb47afffde616060b909e83372 Author: Jack Date: Sun Aug 30 04:48:55 2026 +0000 initial commit diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..23f18ba Binary files /dev/null and b/__pycache__/database.cpython-312.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..2239350 --- /dev/null +++ b/app.py @@ -0,0 +1,310 @@ +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/', 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/', 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/', 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/') +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/') +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) diff --git a/database.py b/database.py new file mode 100644 index 0000000..939c3eb --- /dev/null +++ b/database.py @@ -0,0 +1,132 @@ +import sqlite3 +from datetime import datetime + +DB_PATH = 'snmp_monitor.db' + +def init_db(): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + + # Targets table (devices to monitor) + c.execute(''' + CREATE TABLE IF NOT EXISTS targets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + ip_address TEXT NOT NULL, + port INTEGER DEFAULT 161, + community TEXT NOT NULL, + period_minutes INTEGER DEFAULT 5, + modem_type TEXT DEFAULT 'rv50', + enabled INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Readings table (stored SNMP data) + c.execute(''' + CREATE TABLE IF NOT EXISTS readings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target_id INTEGER NOT NULL, + bytes_sent INTEGER, + bytes_recv INTEGER, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (target_id) REFERENCES targets(id) + ) + ''') + + # Check if modem_type column exists, add it if not (for existing databases) + try: + c.execute('SELECT modem_type FROM targets LIMIT 1') + except sqlite3.OperationalError: + c.execute('ALTER TABLE targets ADD COLUMN modem_type TEXT DEFAULT "rv50"') + conn.commit() + + conn.commit() + conn.close() + +def add_target(name, ip_address, port, community, period_minutes, modem_type='rv50'): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(''' + INSERT INTO targets (name, ip_address, port, community, period_minutes, modem_type) + VALUES (?, ?, ?, ?, ?, ?) + ''', (name, ip_address, port, community, period_minutes, modem_type)) + conn.commit() + target_id = c.lastrowid + conn.close() + return target_id + +def get_all_targets(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute('SELECT * FROM targets ORDER BY id') + targets = c.fetchall() + conn.close() + return targets + +def get_target(target_id): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute('SELECT * FROM targets WHERE id = ?', (target_id,)) + target = c.fetchone() + conn.close() + return target + +def update_target(target_id, name, ip_address, port, community, period_minutes, enabled, modem_type='rv50'): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(''' + UPDATE targets + SET name=?, ip_address=?, port=?, community=?, period_minutes=?, enabled=?, modem_type=? + WHERE id=? + ''', (name, ip_address, port, community, period_minutes, enabled, modem_type, target_id)) + conn.commit() + conn.close() + +def delete_target(target_id): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('DELETE FROM readings WHERE target_id = ?', (target_id,)) + c.execute('DELETE FROM targets WHERE id = ?', (target_id,)) + conn.commit() + conn.close() + +def add_reading(target_id, bytes_sent, bytes_recv): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(''' + INSERT INTO readings (target_id, bytes_sent, bytes_recv) + VALUES (?, ?, ?) + ''', (target_id, bytes_sent, bytes_recv)) + conn.commit() + conn.close() + +def get_readings(target_id, hours=24): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute(''' + SELECT * FROM readings + WHERE target_id = ? + AND timestamp >= datetime('now', '-{} hours') + ORDER BY timestamp + '''.format(hours), (target_id,)) + readings = c.fetchall() + conn.close() + return readings + +def get_latest_reading(target_id): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute(''' + SELECT * FROM readings + WHERE target_id = ? + ORDER BY timestamp DESC + LIMIT 1 + ''', (target_id,)) + reading = c.fetchone() + conn.close() + return reading diff --git a/oldapp.py b/oldapp.py new file mode 100644 index 0000000..949bc7d --- /dev/null +++ b/oldapp.py @@ -0,0 +1,255 @@ +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__) + +# OIDs for Sierra Wireless RV50 +OID_BYTES_SENT = '.1.3.6.1.4.1.20542.9.1.1.2.284.0' +OID_BYTES_RECV = '.1.3.6.1.4.1.20542.9.1.1.2.283.0' + +# 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: + 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 snmp_get(ip, port, community, oid): + """Execute snmpget command and return value""" + 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: + # Parse output: "SNMPv2-SMI::enterprises.20542.9.1.1.2.284.0 = Counter64: 123456" + match = re.search(r'Counter64:\s*(\d+)', result.stdout) + if match: + return int(match.group(1)) + return None + except Exception as e: + print(f"SNMP error: {e}") + return None + +def poll_target(target_id, ip, port, community): + """Poll a target and store the reading""" + global last_readings + + bytes_sent = snmp_get(ip, port, community, OID_BYTES_SENT) + bytes_recv = snmp_get(ip, port, community, OID_BYTES_RECV) + + 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: + 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} + +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']) + +@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)) + + if ip_address: + target_id = db.add_target(name, ip_address, port, community, period_minutes) + + # Schedule polling for this target + scheduler.add_job( + scheduled_poll, + 'interval', + minutes=period_minutes, + id=f'target_{target_id}', + replace_existing=True + ) + + # Do initial poll + poll_target(target_id, ip_address, port, community) + + return redirect(url_for('index')) + +@app.route('/edit/', 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 + + db.update_target(target_id, name, ip_address, port, community, period_minutes, enabled) + + # Update scheduler + 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/', 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/', 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']) + return jsonify(result) + return jsonify({'success': False, 'error': 'Target not found'}), 404 + +@app.route('/graph/') +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/') +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() + + # Start with initial poll for any existing targets + 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']) + + app.run(debug=True, host='0.0.0.0', port=5500) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..38173b6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.0 +apscheduler==3.10.4 diff --git a/snmp_monitor.db b/snmp_monitor.db new file mode 100644 index 0000000..34b23d2 Binary files /dev/null and b/snmp_monitor.db differ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..4d25759 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,41 @@ + + + + + + {% block title %}SNMP Monitor{% endblock %} + + {% block extra_css %}{% endblock %} + + +
+ {% block content %}{% endblock %} +
+ {% block extra_js %}{% endblock %} + + diff --git a/templates/graph.html b/templates/graph.html new file mode 100644 index 0000000..ffdde15 --- /dev/null +++ b/templates/graph.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} + +{% block title %}Graph - {{ target.name }}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +

📊 {{ target.name }} - Data Usage

+ +
+
+ 6h + 24h + 7d + ← Back +
+ + +
+ +
+

Device Info

+

Name: {{ target.name }}

+

Modem Type: {{ target.modem_type or 'rv50' }}

+

IP: {{ target.ip_address }}:{{ target.port }}

+

Community: {{ target.community }}

+

Poll Period: {{ target.period_minutes }} minutes

+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..389e73f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,147 @@ +{% extends 'base.html' %} + +{% block title %}SNMP Monitor - Dashboard{% endblock %} + +{% block content %} +

📡 Cellular Data Usage Monitor

+ + +
+

Add New Target

+
+
+ + + + + + + +
+
+
+ + +
+

Monitored Devices

+ {% if targets %} + + + + + + + + + + + + + + + + + + {% for target in targets %} + + + + + + + + + + + + + + {% endfor %} + +
NameModemIP AddressBytes SentBytes ReceivedRate SentRate RecvPeriodLast PollStatusActions
{{ target.name }}{{ target.modem_type or 'rv50' }}{{ target.ip_address }}:{{ target.port }}{{ target.bytes_sent_fmt }}{{ target.bytes_recv_fmt }}{{ target.rate_sent_fmt }}{{ target.rate_recv_fmt }}{{ target.period_minutes }} min{{ target.last_poll or 'Never' }} + {{ 'Active' if target.enabled else 'Paused' }} + + 📊 Graph + + +
+ +
+
+ {% else %} +

No targets configured. Add one above!

+ {% endif %} +
+ + + +{% endblock %} + +{% block extra_js %} + +{% endblock %}