Add network monitoring, dashboard, and Home Assistant MQTT publishing

Keep /alive unchanged and serve a dashboard at / with status cards,
latency graphs, an event timeline, and a Wi-Fi section.

Background monitors on configurable intervals: TCP reachability,
DNS resolution, public IP change detection, host reboot detection,
Wi-Fi link quality, and Wi-Fi scan of networks in range via iw.
The Wi-Fi interface is selected with WIFI_INTERFACE; the page lists
visible interfaces and reports when none is available. Scan results
stay in memory, other metrics go to SQLite on a mounted volume.

Optional MQTT publishing with Home Assistant discovery groups all
sensors under one device per host.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdvJYoY8kc8bmBck89U2i6
This commit is contained in:
Milan PandurovandClaude Fable 5.1 committed 2026-09-17 15:42:09 +02:00
1 parent aace768660
commit 7206e0c4db
13 files changed
+1509 -6

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
# healthcheck-container
Small Flask service for remote devices (Raspberry Pi in Docker) that answers
`/alive` for container monitoring and, on top of that, watches the network the
device sits on: internet reachability, DNS, public IP changes, Wi-Fi link
quality, Wi-Fi networks in range, and host reboots. Metrics go to SQLite and
are shown on a dashboard at `/`. Optionally the same values are published to an
MQTT broker with Home Assistant discovery, so each device shows up in Home
Assistant as a device with sensors and no manual configuration.
## Endpoints
| Path | Purpose |
| --- | --- |
| `/alive` | Unchanged health check, returns `{"alive": true}` |
| `/` | Dashboard: status, Wi-Fi, graphs, timeline |
| `/api/status` | Latest state of every monitor as JSON |
| `/api/wifi` | Interface list, link details, last scan results |
| `/api/events?limit=200` | Timeline events, newest first |
| `/api/samples?prefix=reach.&range=3600` | Bucketed samples for graphs |
## Running
```sh
docker compose up -d --build
```
Then open `http://<device>:9999/`.
Wi-Fi needs two things from Docker:
- `network_mode: host`, otherwise the container has no wireless interface at all.
- `cap_add: NET_ADMIN`, otherwise link and interface info work but scanning fails
with a permission error that is shown on the dashboard.
Without `WIFI_INTERFACE` the dashboard lists the interfaces it can see and
nothing is scanned. Set the variable to one of the listed wireless interfaces
and restart the container.
## Storage
Samples and events are stored in SQLite at `DB_PATH` (default
`/data/healthcheck.db`). The compose file mounts a named volume at `/data`, so
the database survives `docker compose down`, image rebuilds and `up -d`. Only
`docker compose down -v` or `docker volume rm` deletes it. A bind mount works
as well, for example `- /opt/healthcheck:/data`.
Samples older than `SAMPLE_RETENTION_DAYS` (default 7) are deleted hourly.
Events are kept. Wi-Fi scan results are held in memory only and never written.
## Configuration
All settings are environment variables. Intervals are in seconds.
| Variable | Default | Meaning |
| --- | --- | --- |
| `PORT` | `9999` | HTTP port |
| `DB_PATH` | `/data/healthcheck.db` | SQLite file |
| `SAMPLE_RETENTION_DAYS` | `7` | How long graph samples are kept |
| `DEVICE_NAME` | hostname | Shown on the dashboard and in Home Assistant |
| `DEVICE_ID` | slug of `DEVICE_NAME` | Used in MQTT topics and unique ids |
| `REACH_TARGETS` | `1.1.1.1:443,8.8.8.8:443,9.9.9.9:443` | TCP connect targets, `host:port` |
| `REACH_INTERVAL` | `30` | |
| `REACH_TIMEOUT` | `3` | Connect timeout per target |
| `DNS_NAME` | `cloudflare.com` | Name resolved through the system resolver |
| `DNS_INTERVAL` | `60` | |
| `PUBLIC_IP_URLS` | ipify, ifconfig.me, icanhazip | Tried in order, first answer wins |
| `PUBLIC_IP_INTERVAL` | `300` | |
| `PUBLIC_IP_TIMEOUT` | `5` | |
| `UPTIME_INTERVAL` | `60` | Reboot detection via `/proc/uptime` |
| `WIFI_INTERFACE` | empty | Wireless interface to use, empty disables Wi-Fi |
| `WIFI_LINK_INTERVAL` | `30` | Signal, bitrate, retries of the current link |
| `WIFI_SCAN_INTERVAL` | `60` | Scan for networks in range |
| `MQTT_HOST` | empty | Broker host, empty disables MQTT |
| `MQTT_PORT` | `1883` | |
| `MQTT_USERNAME` | empty | |
| `MQTT_PASSWORD` | empty | |
| `MQTT_DISCOVERY_PREFIX` | `homeassistant` | Must match the MQTT integration setting |
| `LOG_LEVEL` | `INFO` | |
## Events on the timeline
- Internet unreachable / reachable again (all targets failed, then any succeeded)
- DNS resolution failed / working again
- Public IP is X, Public IP changed from X to Y
- Wi-Fi connected, disconnected, roamed to another BSSID
- Host rebooted
- Health check service started
## Home Assistant
Set `MQTT_HOST` (and credentials if the broker needs them). On connect the
service publishes retained discovery messages under
`homeassistant/<component>/healthcheck_<device_id>/<key>/config`, all pointing
to one device, then publishes state to `healthcheck/<device_id>/state` and
availability to `healthcheck/<device_id>/availability`. When Home Assistant
restarts it announces itself on `homeassistant/status` and the service
re-publishes discovery.
Entities per device:
- Internet, DNS, Wi-Fi (binary sensors, `connectivity` class)
- Internet latency, DNS latency (ms)
- Public IP
- Host uptime
- Wi-Fi SSID, BSSID, channel, signal (dBm), TX bitrate (Mbit/s), networks in range
## Development
```sh
python3 -m unittest discover -s tests
DB_PATH=./data/hc.db python3 app.py
```