From 25f016595f7c09d9486da3274b95d06f11ee5851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Bu=C3=9Fmann?= Date: Sun, 4 May 2025 22:38:47 +0200 Subject: [PATCH] simple network logging app --- .gitignore | 3 + app.py | 38 +++++++++++ com.user.speedtest.plist | 26 ++++++++ config.py.example | 2 + db.py | 89 ++++++++++++++++++++++++ run_speedtest.py | 24 +++++++ static/css/style.css | 27 ++++++++ templates/index.html | 141 +++++++++++++++++++++++++++++++++++++++ templates/layout.html | 21 ++++++ 9 files changed, 371 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 com.user.speedtest.plist create mode 100644 config.py.example create mode 100644 db.py create mode 100755 run_speedtest.py create mode 100644 static/css/style.css create mode 100644 templates/index.html create mode 100644 templates/layout.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06c169b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +config.py +speedtest.db diff --git a/app.py b/app.py new file mode 100644 index 0000000..bb91a81 --- /dev/null +++ b/app.py @@ -0,0 +1,38 @@ +from flask import Flask, render_template +import sqlite3 +import pandas as pd +from config import DB_PATH +from datetime import datetime + +app = Flask(__name__) + +def load_data(): + # Connect to SQLite database + conn = sqlite3.connect(DB_PATH) + query = "SELECT * FROM speed_tests" + df = pd.read_sql(query, conn) + conn.close() + return df + +@app.route('/') +def index(): + df = load_data() + + + + # Convert timestamps to human-readable format + df['datetime'] = pd.to_datetime(df['timestamp'], unit='s') + # Suppose your DataFrame is called `df` + df["timestamp"] = df["timestamp"].apply(lambda ts: datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")) + + # Collect the data for charts + chart_data = { + "times": df['datetime'].dt.strftime('%Y-%m-%d %H:%M:%S').tolist(), + "down_90th": df['down_90th'].tolist(), + "up_90th": df['up_90th'].tolist() + } + + return render_template('index.html', data=df.to_dict(orient='records'), chart_data=chart_data) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/com.user.speedtest.plist b/com.user.speedtest.plist new file mode 100644 index 0000000..1f3a09f --- /dev/null +++ b/com.user.speedtest.plist @@ -0,0 +1,26 @@ + + + + + Label + com.user.speedtest + + ProgramArguments + + /Users/cato/Code/Cato447/speed-logger/run_speedtest.py + + + StartInterval + 30 + + RunAtLoad + + + StandardOutPath + /tmp/speedtest.out + StandardErrorPath + /tmp/speedtest.err + + + diff --git a/config.py.example b/config.py.example new file mode 100644 index 0000000..ea76945 --- /dev/null +++ b/config.py.example @@ -0,0 +1,2 @@ +DB_PATH = +ROUTER_MAC = diff --git a/db.py b/db.py new file mode 100644 index 0000000..8b38f18 --- /dev/null +++ b/db.py @@ -0,0 +1,89 @@ +import sqlite3 +from config import DB_PATH + + +def init_db(db_path=DB_PATH): + conn = sqlite3.connect(db_path) + c = conn.cursor() + c.execute(''' + CREATE TABLE IF NOT EXISTS speed_tests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL NOT NULL, + failed BOOLEAN NOT NULL, + isp TEXT, + ip TEXT, + location_code TEXT, + location_city TEXT, + location_region TEXT, + latency REAL, + jitter REAL, + down_100kB REAL, + down_1MB REAL, + down_10MB REAL, + down_25MB REAL, + down_90th REAL, + up_100kB REAL, + up_1MB REAL, + up_10MB REAL, + up_90th REAL + ) + ''') + conn.commit() + conn.close() + + +def insert_result(results: dict|None, db_path=DB_PATH): + conn = sqlite3.connect(db_path) + c = conn.cursor() + + # If the test failed entirely, store it as a failure with timestamp now + if results is None or "tests" not in results: + from time import time + c.execute("INSERT INTO speed_tests (timestamp, failed) VALUES (?, ?)", (time(), True)) + conn.commit() + conn.close() + return + + tests = results.get("tests", {}) + meta = results.get("meta", {}) + + # Get a consistent timestamp from any TestResult (or fallback to now) + from time import time as now + sample_test = next(iter(tests.values()), None) + timestamp = sample_test.time if sample_test else now() + + print(tests) + print(meta) + + def get(tests, key): + return tests[key].value if key in tests else None + + c.execute(''' + INSERT INTO speed_tests ( + timestamp, failed, isp, ip, location_code, location_city, location_region, + latency, jitter, + down_100kB, down_1MB, down_10MB, down_25MB, down_90th, + up_100kB, up_1MB, up_10MB, up_90th + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + timestamp, False, + get(tests, "isp"), + get(meta, "ip"), + get(meta, "location_code"), + get(meta, "location_city"), + get(meta, "location_region"), + get(tests, "latency"), + get(tests, "jitter"), + get(tests, "100kB_down_mbps"), + get(tests, "1MB_down_mbps"), + get(tests, "10MB_down_mbps"), + get(tests, "25MB_down_mbps"), + get(tests, "90th_percentile_down_mbps"), + get(tests, "100kB_up_mbps"), + get(tests, "1MB_up_mbps"), + get(tests, "10MB_up_mbps"), + get(tests, "90th_percentile_up_mbps") + )) + conn.commit() + conn.close() + diff --git a/run_speedtest.py b/run_speedtest.py new file mode 100755 index 0000000..8d23756 --- /dev/null +++ b/run_speedtest.py @@ -0,0 +1,24 @@ +#! /Users/cato/Code/Cato447/speed-logger/.venv/bin/python3 + +from cfspeedtest import CloudflareSpeedtest +from db import init_db, insert_result +from getmac import get_mac_address +from config import ROUTER_MAC + +def run_test_and_save(): + if get_mac_address(ip="192.168.0.1") != ROUTER_MAC: + print(get_mac_address(ip="192.168.0.1"), ROUTER_MAC) + print("Not connected to home network") + return + try: + tester = CloudflareSpeedtest() + results = tester.run_all(megabits=True) # returns SuiteResults + except Exception: + results = None # Trigger a failed test record + + init_db() + insert_result(results) + +print("==== Running Speedtest ====") +run_test_and_save() +print("==== Speedtest ended ====") diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..4a9c5c1 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,27 @@ +body { + font-family: Arial, sans-serif; + padding: 20px; + background-color: #f4f4f4; +} + +header h1 { + text-align: center; + color: #333; +} + +table { + width: 100%; + margin-top: 20px; + border-collapse: collapse; +} + +th, td { + padding: 10px; + text-align: center; + border: 1px solid #ddd; +} + +th { + background-color: #f2f2f2; +} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..d1fba2b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,141 @@ +{% extends 'layout.html' %} + +{% block content %} +

Graph: Download vs. Upload Speeds

+ + +

Test Results

+ + + + + + + + + + + + + + {% for row in data %} + + + + + + + + + {% endfor %} + +
TimestampISPLatencyJitterDownload (90th Percentile)Upload (90th Percentile)
{{ row.timestamp }}{{ row.isp }}{{ row.latency }}{{ row.jitter }}{{ row.down_90th }}{{ row.up_90th }}
+ + + +{% endblock %} + diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..9f8d287 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,21 @@ + + + + + + Speed Test Results + + + + +
+

Network Speed Test Results

+
+ +
+ {% block content %} + {% endblock %} +
+ + +