Skip to content
 
 

Repository files navigation

netmon logo

netmon

Self-hosted local network monitor with 24-hour speed charts & sarcastic AI commentary, delivered to Telegram, Discord, or a local web dashboard.

License MIT Python uv Telegram Discord SQLite Matplotlib


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, Discord, or your own local web page — nothing leaves your network with the web option).


This is a fork of Role1776/netmon, with a local web dashboard notifier and a systemd service template added.

Features & Workflow

Every 30 minutes (SLEEP_TIME in main.py, default 1800 seconds):

  1. Speed Test: Measures download/upload speeds, ping latency, ISP, and test server details using the official Ookla speedtest CLI (see the note on measurement mode).
  2. LAN Scan: Scans the local subnet using nmap ARP scan to count active connected devices.
  3. Local Storage: Saves metrics & device tallies directly to a local metrics.sql SQLite database.
  4. Status Alert: Sends a concise status update to your chosen notifier ("all good" or "line is dying").
  5. 24h AI Report: Every 8th cycle (every 4h), generates a 24-hour trend graph via matplotlib alongside 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)
Ookla speedtest CLI (official) 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 --iflist required; Windows not supported out of the box).
  • uv — manages the Python version, virtualenv, and locked dependencies for you. No manual python3/venv/pip juggling.
  • System Binaries: nmap and the official Ookla speedtest CLI installed system-wide.

    [!WARNING] This must be Ookla's own CLI (package name speedtest, installed via their packagecloud.io repo below) — not the older, unrelated speedtest-cli Python package from apt/pip. Both install a command that may be called speedtest, but they produce different JSON output, and the older one relies on a legacy Speedtest.net server list that has become unreliable (thin, stale, and prone to routing tests through broken third-party mirror servers with wildly inaccurate results). If speedtest --version doesn't print "Speedtest by Ookla", you have the wrong one.

  • Passwordless sudo for nmap — 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) — or neither, if you use the built-in local web page — plus an API key for your OpenAI-compatible provider (not needed if you point AI_BASE_URL at a local LLM server).
  • Optional, for the web notifier: a static file server such as Apache to serve the generated reports (see Web (local dashboard)).

Quick Start

1. System Dependencies

macOS (Homebrew):

brew install nmap
brew tap teamookla/speedtest
brew install speedtest --force

Linux (Debian/Ubuntu, including Raspberry Pi OS):

sudo apt update && sudo apt install -y nmap curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt-get install -y speedtest

If you previously had the unofficial speedtest-cli apt package installed, remove it first (sudo apt-get remove speedtest-cli) to avoid a conflicting speedtest command.

Accept Ookla's license once, interactively, so it never prompts (and hangs) later when run unattended by the service:

speedtest --accept-license --accept-gdpr

2. 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 | sh

Then:

git clone https://github.com/Role1776/netmon.git
cd netmon
uv sync

uv 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:

cp .env.example .env

.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), discord, or web — 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
WEB_OUTPUT_DIR Optional. Folder for the generated dashboard when NOTIFIER=web (default webout)
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 main.py

uv run always uses this project's own .venv and pinned Python version, so it can't accidentally run against your system python3.

Tip

Running it in a foreground terminal (or tmux/screen) is fine for testing, but it'll stop the moment you log out. For 24/7 unattended use, run it as a systemd service — see below.

Running as a systemd service (Linux)

A template unit file is provided at systemd/netmon.service.example. To use it:

  1. Copy it into place and open it for editing:
    sudo cp systemd/netmon.service.example /etc/systemd/system/netmon.service
    sudo nano /etc/systemd/system/netmon.service
  2. Replace the placeholders inside:
    • <path-to-netmon> — the absolute path to your cloned repo, e.g. /home/pi/netmon
    • <uv-path> — the absolute path to your uv binary (find it with which uv)
    • <run-as-user> — the user to run the service as. If you're running the nmap device scan as root (or via the passwordless-sudo rule above from a non-root user), match whichever setup you used in the passwordless nmap step.
  3. Enable and start it:
    sudo systemctl daemon-reload
    sudo systemctl enable netmon
    sudo systemctl start netmon
  4. Check it's running, and follow the logs live:
    sudo systemctl status netmon
    journalctl -u netmon -f

The service is set to restart automatically on failure (e.g. a transient speedtest error), and to start on boot once enabled. After editing any .py file, re-apply changes with:

sudo systemctl restart netmon

Notifications: Telegram, Discord, or a Local Web Page

netmon supports three notification backends, selected via the NOTIFIER variable in .env. Only one is needed.

Telegram (default)

  1. Message @BotFather on Telegram and send /newbot, following the prompts to get a bot token.
  2. Get your Chat ID — the simplest way is to message your new bot, then visit https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates in a browser and read the chat.id field from the JSON response.
  3. 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

  1. In your target Discord channel: Server Settings → Integrations → Webhooks → New Webhook, then copy the webhook URL. No bot invite or permissions setup needed.
  2. 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).

Web (local dashboard)

If you'd rather view reports in a browser on your own network than get pushed messages, use the built-in web notifier — no bot token, webhook, or external service required.

  1. In .env:
    NOTIFIER=web
    WEB_OUTPUT_DIR=webout
    
  2. Run netmon at least once (directly or as the systemd service) so the output folder exists:
    uv run main.py
    It will create <path-to-netmon>/webout/ containing index.html, the latest graph image, and a rolling history of past reports (newest first, capped at 20 entries).
  3. Point a static file server at that folder. With Apache already installed:
    sudo ln -s /path/to/netmon/webout /var/www/html/netmon
    Then make sure Apache is allowed to follow the symlink — in /etc/apache2/apache2.conf, the <Directory /var/www/> block should include FollowSymLinks:
    <Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>
    Restart Apache after any change: sudo systemctl restart apache2.
  4. If netmon runs as a non-root user, Apache's www-data user also needs permission to traverse into the folder:
    chmod o+x /path/to/netmon
    chmod -R o+rX /path/to/netmon/webout
  5. Visit http://<device-hostname-or-ip>/netmon in a browser on the same network.

The page auto-refreshes every 5 minutes and needs no JavaScript framework or backend — netmon just (re)writes static HTML/PNG files on disk each cycle.

Note

This serves the dashboard on your local network only, with no authentication. Don't port-forward it to the public internet as-is.


A Note on Measurement Mode

netmon runs the official Ookla speedtest CLI (see runner.py), which — like the speedtest.net website and app — opens multiple parallel connections per test. That measures the practical ceiling of your line, which is usually the number you'd expect to see and compare against your ISP's advertised speed.

This is a deliberate choice, and a change from earlier versions of this project (and its upstream) that used the older, unofficial speedtest-cli Python package in single-connection mode. Single-stream testing has a real rationale — it approximates what one application on your network actually gets, since it's subject to the same window-size and packet-loss limits as any ordinary download, and multi-threaded tests can overstate that. But the older tool's underlying server list has degraded over time (thin, stale, increasingly routed through unreliable third-party mirrors) to the point where its readings became actively misleading — see the GitHub issue tracking this for other affected projects. The official CLI's actively-maintained server network is the more trustworthy foundation, even though it changes what the numbers represent.

Two consequences worth knowing:

  • Multi-threaded results will typically read higher than single-connection ones did, especially on fast links (500 Mbps+) — this is expected, not a sign your connection improved.
  • If you're migrating from a fork/version using the old backend, expect a visible step-change in your 24-hour graph the day you switch, and the AI commentary may describe it as a real speed jump since it has no way to know the measurement method changed underneath it. That's a one-time artifact of the switch, not a fault in your line.

Since netmon exists to track trends, consistency matters more than any single figure: pick one measurement method and stick with it for the lifetime of your database.


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:

24h Network Speed Test Graph

<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                      # Ookla 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
├── web.py                         # Local static-HTML dashboard notifier
├── config.py                      # Environment variable validation & config
├── notifier.py                    # Notifier protocol & shared chat-action enum
├── systemd/
│   └── netmon.service.example     # Template systemd unit for 24/7 unattended running
├── 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.

About

Self-hosted network monitor - hourly speed tests, LAN device counts via ARP scan, and sarcastic AI-generated reports delivered to Telegram or Discord.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages