Files
server-monitor/checks/nut.py
T
MrYoshii c756195530 Initial commit: server-monitor v1 (Python)
Kompletter Neustart als Python-Projekt.
Portiert von zfs-monitor Bash v3, saubere Architektur:
- config.py: TOML-Config mit dataclasses
- state.py: zentrales State-Management (JSON statt Einzeldateien)
- notify.py: Telegram + Persistent Alerting als wiederverwendbare Klasse
- checks/zfs.py: ZFS Pool-Health, I/O-Fehler, Kapazität
- checks/smart.py: SATA + NVMe SMART-Monitoring
- checks/nut.py: NUT/USV-Monitoring (APC Back-UPS RS 1500G)
2026-04-12 04:15:02 +02:00

170 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from checks.base import Check
from state import make_key
class NutCheck(Check):
def run(self) -> None:
for ups in self.config.nut.ups:
self.log.info("--- USV: %s ---", ups)
self._check_ups(ups)
def _check_ups(self, ups_name: str) -> None:
key_conn = make_key("nut_conn", ups_name)
key_ob = make_key("nut_ob", ups_name)
key_rb = make_key("nut_rb", ups_name)
key_batt = make_key("nut_batt", ups_name)
key_load = make_key("nut_load", ups_name)
key_runtime = make_key("nut_rt", ups_name)
raw = self._run(["upsc", ups_name], timeout=self.config.nut.timeout)
if not raw or raw.strip() == "":
if self.alerter.should_alert(key_conn, "WARNING"):
self.alerter.send_alert(
"WARNING",
f"USV nicht erreichbar: {ups_name}",
f"Keine Verbindung zu NUT-USV <code>{ups_name}</code>!\n\n"
f"Läuft der Dienst? → <code>systemctl status nut-server</code>\n"
f"<code>upsc {ups_name}</code>",
key_conn,
)
return
else:
if self.alerter.clear_alert(key_conn):
self.alerter.send_alert(
"INFO",
f"USV wieder erreichbar: {ups_name}",
f"NUT-USV <code>{ups_name}</code> ist wieder erreichbar ✅",
)
# Werte parsen
values: dict[str, str] = {}
for line in raw.splitlines():
if ": " in line:
k, _, v = line.partition(": ")
values[k.strip()] = v.strip()
status = values.get("ups.status", "")
batt_charge = _to_int(values.get("battery.charge", "100"))
batt_runtime = _to_int(values.get("battery.runtime", "9999"))
ups_load = _to_int(values.get("ups.load", "0"))
# ── On Battery ───────────────────────────────────────
if "OB" in status:
runtime_min = batt_runtime // 60
lb_hint = "\n⚠️ <b>LOW BATTERY Shutdown droht!</b>" if "LB" in status else ""
if self.alerter.should_alert(key_ob, "CRITICAL"):
self.alerter.send_alert(
"CRITICAL",
f"USV {ups_name}: STROMAUSFALL!",
f"<b>Netzstrom ausgefallen!</b> USV läuft auf Batterie.{lb_hint}\n\n"
f"🔋 Akku: <b>{batt_charge}%</b>\n"
f"⏱️ Restlaufzeit: <b>{runtime_min} min</b>\n"
f"⚡ Last: <b>{ups_load}%</b>\n\n"
f"<code>upsc {ups_name}</code>",
key_ob,
)
# Restlaufzeit kritisch
if batt_runtime < self.config.nut.runtime_crit_sec:
if self.alerter.should_alert(key_runtime, "CRITICAL"):
self.alerter.send_alert(
"CRITICAL",
f"USV {ups_name}: Restlaufzeit kritisch!",
f"USV <code>{ups_name}</code>: Nur noch <b>{runtime_min} min</b> Restlaufzeit!\n\n"
f"System sofort herunterfahren:\n"
f"<code>upsmon -c fsd</code>",
key_runtime,
)
else:
if self.alerter.clear_alert(key_ob):
self.alerter.send_alert(
"INFO",
f"USV {ups_name}: Netzstrom wiederhergestellt ✅",
f"Netzstrom wieder vorhanden. USV <code>{ups_name}</code> "
f"zurück im Normalbetrieb.",
)
self.alerter.clear_alert(key_runtime)
# ── Replace Battery ───────────────────────────────────
if "RB" in status:
if self.alerter.should_alert(key_rb, "WARNING"):
self.alerter.send_alert(
"WARNING",
f"USV {ups_name}: Batterie ersetzen!",
f"USV <code>{ups_name}</code> meldet: <b>Batterie muss ersetzt werden!</b>\n\n"
f"Aktueller Ladestand: {batt_charge}%\n"
f"<code>upsc {ups_name}</code>",
key_rb,
)
else:
if self.alerter.clear_alert(key_rb):
self.alerter.send_alert(
"INFO",
f"USV {ups_name}: Batterie-Status OK ✅",
f"USV <code>{ups_name}</code>: Replace-Battery Flag wurde aufgehoben.",
)
# ── Akkustand (nur im Netzbetrieb) ────────────────────
if "OB" not in status:
cfg = self.config.nut
if batt_charge <= cfg.batt_crit_pct:
if self.alerter.should_alert(key_batt, "CRITICAL"):
self.alerter.send_alert(
"CRITICAL",
f"USV {ups_name}: Akkustand kritisch!",
f"USV <code>{ups_name}</code>: Akkustand <b>{batt_charge}%</b> "
f"≤ {cfg.batt_crit_pct}%!\n\nAkku lädt nicht korrekt?",
key_batt,
)
elif batt_charge <= cfg.batt_warn_pct:
if self.alerter.should_alert(key_batt, "WARNING"):
self.alerter.send_alert(
"WARNING",
f"USV {ups_name}: Akkustand niedrig",
f"USV <code>{ups_name}</code>: Akkustand <b>{batt_charge}%</b> "
f"≤ {cfg.batt_warn_pct}%",
key_batt,
)
else:
if self.alerter.clear_alert(key_batt):
self.alerter.send_alert(
"INFO",
f"USV {ups_name}: Akkustand OK ✅",
f"USV <code>{ups_name}</code>: Akkustand wieder bei {batt_charge}% ✅",
)
# ── Last ──────────────────────────────────────────────
if ups_load >= self.config.nut.load_warn_pct:
if self.alerter.should_alert(key_load, "WARNING"):
self.alerter.send_alert(
"WARNING",
f"USV {ups_name}: Hohe Last",
f"USV <code>{ups_name}</code>: Last <b>{ups_load}%</b> "
f"≥ {self.config.nut.load_warn_pct}%",
key_load,
)
else:
if self.alerter.clear_alert(key_load):
self.alerter.send_alert(
"INFO",
f"USV {ups_name}: Last normal ✅",
f"USV <code>{ups_name}</code>: Last wieder unter "
f"{self.config.nut.load_warn_pct}% ({ups_load}%) ✅",
)
self.log.info(
"USV %s: status=%s batt=%d%% runtime=%ds load=%d%%",
ups_name, status, batt_charge, batt_runtime, ups_load,
)
def _to_int(val: str) -> int:
"""Parse int from NUT value, stripping decimals and units."""
try:
return int(float(val.split()[0]))
except (ValueError, IndexError):
return 0