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:
1 parent
aace768660
commit
7206e0c4db
13 files changed
+1509
-6
No files matched your search
+106
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
from contextlib import closing
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS samples (
|
||||
ts REAL NOT NULL,
|
||||
metric TEXT NOT NULL,
|
||||
value REAL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS samples_metric_ts ON samples(metric, ts);
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts REAL NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS events_ts ON events(ts);
|
||||
CREATE TABLE IF NOT EXISTS kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def _like_prefix(prefix):
|
||||
escaped = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
return escaped + "%"
|
||||
|
||||
|
||||
class Storage:
|
||||
def __init__(self, path):
|
||||
directory = os.path.dirname(path)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
self.path = path
|
||||
with closing(self._connect()) as conn:
|
||||
conn.executescript(SCHEMA)
|
||||
|
||||
def _connect(self):
|
||||
conn = sqlite3.connect(self.path, timeout=10)
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
return conn
|
||||
|
||||
def add_samples(self, pairs, ts=None):
|
||||
ts = ts or time.time()
|
||||
with closing(self._connect()) as conn, conn:
|
||||
conn.executemany(
|
||||
"INSERT INTO samples (ts, metric, value) VALUES (?, ?, ?)",
|
||||
[(ts, metric, value) for metric, value in pairs],
|
||||
)
|
||||
|
||||
def add_sample(self, metric, value, ts=None):
|
||||
self.add_samples([(metric, value)], ts)
|
||||
|
||||
def add_event(self, kind, message, ts=None):
|
||||
with closing(self._connect()) as conn, conn:
|
||||
conn.execute(
|
||||
"INSERT INTO events (ts, kind, message) VALUES (?, ?, ?)",
|
||||
(ts or time.time(), kind, message),
|
||||
)
|
||||
|
||||
def events(self, limit=200):
|
||||
with closing(self._connect()) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT ts, kind, message FROM events ORDER BY ts DESC, id DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
return [{"ts": ts, "kind": kind, "message": message} for ts, kind, message in rows]
|
||||
|
||||
def samples_by_prefix(self, prefix, since, bucket):
|
||||
bucket = max(1, int(bucket))
|
||||
with closing(self._connect()) as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT metric, CAST(ts / ? AS INTEGER) * ? AS bucket, AVG(value)
|
||||
FROM samples
|
||||
WHERE metric LIKE ? ESCAPE '\\' AND ts >= ?
|
||||
GROUP BY metric, bucket
|
||||
ORDER BY metric, bucket
|
||||
""",
|
||||
(bucket, bucket, _like_prefix(prefix), since),
|
||||
).fetchall()
|
||||
series = {}
|
||||
for metric, ts, value in rows:
|
||||
series.setdefault(metric, []).append([ts, value])
|
||||
return series
|
||||
|
||||
def get(self, key):
|
||||
with closing(self._connect()) as conn:
|
||||
row = conn.execute("SELECT value FROM kv WHERE key = ?", (key,)).fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
def set(self, key, value):
|
||||
with closing(self._connect()) as conn, conn:
|
||||
conn.execute(
|
||||
"INSERT INTO kv (key, value) VALUES (?, ?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
(key, value),
|
||||
)
|
||||
|
||||
def prune_samples(self, days):
|
||||
cutoff = time.time() - days * 86400
|
||||
with closing(self._connect()) as conn, conn:
|
||||
conn.execute("DELETE FROM samples WHERE ts < ?", (cutoff,))
|
||||
Reference in new issue
Block a user