Self-hosted local network monitor with 24-hour speed charts & sarcastic AI commentary delivered straight to Telegram or Discord.
A lightweight local bot that runs a speed test on your network every 30 minutes, scans active devices on your LAN using nmap, and logs everything to a local SQLite database.
Every 4 hours, it delivers a detailed report complete with a 24-hour trend graph and a sarcastic, LLM-generated commentary on your network's behavior ("someone's hogging the bandwidth again").
Note
100% Private & Self-Hosted: No external metric servers involved — everything runs locally on your machine or Raspberry Pi. Only text reports and graph images are dispatched to your chosen notifier (Telegram or Discord).
Features & Workflow
Every 30 minutes (SLEEP_TIME in main.py, default 1800 seconds):
- Speed Test: Measures download/upload speeds, ping latency, ISP, and test server details using
speedtest-cli(see the note on measurement mode). - LAN Scan: Scans the local subnet using
nmapARP scan to count active connected devices. - Local Storage: Saves metrics & device tallies directly to a local
metrics.sqlSQLite database. - Status Alert: Sends a concise status update to your chosen notifier ("all good" or "line is dying").
- 24h AI Report: Every 8th cycle (every 4h), generates a 24-hour trend graph via
matplotlibalongside a sarcastic LLM analysis of network load and speed fluctuations.
Tech Stack
| Technology | Purpose |
|---|---|
Python 3.13+ (via uv) |
Core runtime |
| SQLite | Local metrics persistence (metrics.sql) |
speedtest-cli |
Network bandwidth and ping measurements |
nmap |
Subnet ARP scanning for device discovery |
matplotlib |
24-hour metrics visualization |
| OpenAI-compatible API | Sarcastic report & trend analysis (cloud OpenAI or a local LLM) |
| Telegram API / Discord Webhooks | Alert and graph report delivery |
Requirements
- OS: macOS or Linux (
nmap --iflistrequired; Windows not supported out of the box). - uv — manages the Python version, virtualenv, and locked dependencies for you. No manual
python3/venv/pipjuggling. - System Binaries:
nmapandspeedtest-cliinstalled system-wide. - Passwordless
sudofornmap— device counting needs a real ARP scan (raw sockets), which requires root; see one-time setup below. - Tokens: either a Telegram Bot Token + Chat ID, or a Discord Webhook URL (see Notifications), plus an API key for your OpenAI-compatible provider (not needed if you point
AI_BASE_URLat a local LLM server).
Quick Start
1. System Dependencies
macOS (Homebrew):
brew install nmap speedtest-cli
Linux (Debian/Ubuntu):
sudo apt update && sudo apt install -y nmap speedtest-cli2. Allow Passwordless nmap (one-time)
Device counting runs nmap as root for a real ARP scan — without it, host discovery silently falls back to ordinary TCP probing and undercounts devices that don't answer on common ports. Since the bot runs unattended, sudo needs to work without a password prompt on every cycle:
echo "$(whoami) ALL=(root) NOPASSWD: $(command -v nmap)" | sudo tee /etc/sudoers.d/netmon-nmap sudo chmod 440 /etc/sudoers.d/netmon-nmap
This grants passwordless sudo only for the nmap binary — not your whole account.
3. Clone & Setup Environment
Install uv if you don't have it yet:
curl -LsSf https://astral.sh/uv/install.sh | shThen:
git clone https://github.com/Role1776/netmon.git
cd netmon
uv syncuv sync downloads the pinned Python version (see .python-version) if you don't already have it, creates .venv, and installs the exact locked dependency versions from uv.lock. No system python3, no manual venv activation.
4. Configure .env
Copy the template file and fill in your secrets:
.env variables:
| Variable | Description |
|---|---|
AI_API_KEY |
Your LLM provider API key (any string works for most local servers) |
AI_MODEL |
Model name (e.g. gpt-4o-mini, or a local model name — see below) |
AI_BASE_URL |
Base API URL (e.g., https://api.openai.com/v1, or your local server's URL) |
NOTIFIER |
telegram (default) or discord — picks which service receives alerts |
TG_BOT_TOKEN |
Telegram bot token from @BotFather — required if NOTIFIER=telegram |
TG_CHAT_ID |
Your Telegram Chat ID — required if NOTIFIER=telegram |
DISCORD_WEBHOOK_URL |
Discord channel webhook URL — required if NOTIFIER=discord |
DB_PATH |
SQLite database file path (e.g. metrics.sql) |
REQUEST_TIMEOUT |
Optional. HTTP timeout in seconds for Telegram/Discord requests (positive integer, default 30) |
Tip
You're not locked into OpenAI. ai.py talks to any OpenAI-compatible endpoint, so a local inference server (e.g. Ollama, LM Studio) works too — just point AI_BASE_URL at it. For report quality that holds up, use a model with at least ~7B parameters; a solid local pick is Gemma 4 12B at 4-bit (QAT) quantization (gemma4:12b-it-qat via Ollama), which fits comfortably on 16GB of RAM.
5. Run the Bot
uv run always uses this project's own .venv and pinned Python version, so it can't accidentally run against your system python3.
Tip
Run the bot inside tmux/screen or set it up as a system service (systemd/launchd) to keep it running 24/7 in the background.
Notifications: Telegram or Discord
netmon supports two notification backends, selected via the NOTIFIER variable in .env. Only one is needed.
Telegram (default)
- Message
@BotFatheron Telegram and send/newbot, following the prompts to get a bot token. - Get your Chat ID — the simplest way is to message your new bot, then visit
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdatesin a browser and read thechat.idfield from the JSON response. - In
.env:NOTIFIER=telegram TG_BOT_TOKEN=123456789:AAHfoo... TG_CHAT_ID=987654321
If NOTIFIER is left unset, netmon defaults to Telegram, so existing setups keep working with no changes.
Discord
- In your target Discord channel: Server Settings → Integrations → Webhooks → New Webhook, then copy the webhook URL. No bot invite or permissions setup needed.
- In
.env:NOTIFIER=discord DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxxx/yyyy
Discord delivery reuses the same report content as Telegram — the existing HTML formatting (<b>, <code>, <pre>) is automatically converted to Discord markdown, so reports render correctly in either service without any changes to the AI prompt.
Warning
Treat both the Telegram bot token and the Discord webhook URL as secrets — anyone with either can post messages through your bot/webhook. Don't commit them to version control (.env is already git-ignored).
A Note on Measurement Mode
netmon runs speedtest --secure --single --json (see runner.py) — the --single flag means the test uses one TCP connection. This is deliberate: a single stream approximates what one real application on your network would actually get, since it is subject to the same window-size and packet-loss limits any ordinary download faces.
Multi-threaded speed tests (including Ookla's official CLI, and the speedtest.net web UI) open many parallel connections instead. That measures something different — the practical ceiling of your line — and will report noticeably higher numbers on fast connections. Neither figure is "wrong"; they answer different questions.
Two consequences worth knowing:
- Don't compare netmon's numbers directly against speedtest.net in a browser. The browser test is multi-threaded and will read higher. That gap is methodology, not a fault in your line.
- On very fast links (roughly 500 Mbps+), expect single-stream figures to sit well below your subscribed speed. Beyond the methodology gap,
speedtest-cliis pure Python, so at gigabit speeds its own CPU overhead starts contributing too.
Since netmon exists to track trends, consistency matters more than peak numbers: keep one measurement method for the lifetime of your database. Swapping the backend mid-history puts a step change in your 24-hour graph that the AI commentary will faithfully report as a real speed jump.
Example Output
Hourly Short Status Update
Network Status Update
Time: 2026-07-21 14:00:00
ISP: MyISP | Server: New York
Devices online: 7
Download: 145.2 Mbps
Upload: 62.1 Mbps
Latency: 14.8 ms
Traffic used: 160.0 MB down / 70.0 MB up
Current status: Good speed and low latency
4-Hour Detailed Report (With Graph & AI Analysis)
Every 4 hours, the bot sends a 24-hour matplotlib graph accompanied by a sarcastic LLM-generated report:
<b>Network Speed Test Report (24h Analysis)</b> Client: <b>MyISP</b> Server: <b>New York</b> <b>Latest Test Metrics</b> <pre> Download: 178.5 Mbps Upload: 45.2 Mbps Ping: 23.1 ms Devices Online: 9 </pre> <b>24-Hour Dynamics Analysis</b> Over the last 24 hours, the download speed averaged <code>140 Mbps</code>, but we saw a massive drop to <code>20 Mbps</code> at 8:00 PM right as device count jumped from <code>4</code> to <code>11 devices</code>. Clearly, someone's hogging the bandwidth or the ISP's mice were busy chewing on the fiber line again. Latency remained stable except for a brief spike during peak hours. <b>Data Transfer (Latest Test)</b> <pre> Downloaded: 160.0 MB Uploaded: 70.0 MB </pre> <b>Conclusion</b> Expect periodic speed drops whenever local freeloaders stream 4K movies or the ISP potato infrastructure struggles.
Project Structure
netmon/
├── assets/ # Logo & documentation media assets
├── graphs/ # Generated 24h matplotlib graph images
├── main.py # Main execution loop & orchestrator
├── runner.py # Speedtest-cli and nmap scan execution & parsing
├── sqlite.py # SQLite database operations & schema management
├── models.py # Domain data models (NetworkMetric, SpeedTest)
├── graphs.py # Matplotlib graph rendering engine
├── ai.py # OpenAI API client & sarcastic text generator
├── tg.py # Telegram bot dispatch helper
├── discord_hook.py # Discord webhook dispatch helper
├── config.py # Environment variable validation & config
├── notifier.py # Notifier protocol & shared chat-action enum
├── pyproject.toml # Project metadata & dependencies
├── uv.lock # Locked, reproducible dependency versions
└── LICENSE # MIT License file
License
Distributed under the MIT License. See LICENSE for more details.

