Hausmeister: Vitalchecks Zigbee-Heizung CT152

API/z2m-Erreichbarkeit, erwartete TRVs online, Batterie <20%,
Abwesenheit ohne Backup — Telegram wie übrige Monitoring-Alarme.
This commit is contained in:
Homelab Cursor 2026-08-02 00:11:09 +02:00
parent f0abeb1255
commit 03099808f0

View file

@ -24,6 +24,7 @@ ALERT_COOLDOWN_SECONDS = {
"default": 3600,
"error_rate": 1800,
"hermes": 7200,
"heizung": 7200,
"backup": 21600,
"mirror_sync": 21600,
}
@ -102,6 +103,117 @@ def _is_host_suppressed(host: str, suppressed_hosts: set) -> bool:
return False
# Zigbee-Heizung CT152 (heizsteuerung) — erwartet online; Platzhalter ohne TRV weglassen
HEIZ_API_BASES = [
os.environ.get("HEIZSTEUERUNG_API_BASE", "").rstrip("/"),
"http://100.105.175.123:8090", # Tailscale CT152
"http://192.168.178.30:8090", # LAN (nur wenn erreichbar)
]
HEIZ_API_BASES = [u for u in HEIZ_API_BASES if u]
HEIZ_Z2M_URLS = [
"http://100.105.175.123:8080",
"http://192.168.178.30:8080",
]
HEIZ_EXPECTED_ONLINE = {
"wohnstube_links",
"wohnstube_rechts",
"schlafzimmer",
"flur_unten",
"trockenraum",
"buero",
"ankleideraum_oben",
"wc_unten",
}
HEIZ_BATTERY_WARN = 20
def check_heizkoerper() -> list[str]:
"""Vitalfunktionen Zigbee-Heizung CT152: API, z2m, TRV-Online, Batterie."""
alerts: list[str] = []
headers = {"User-Agent": "Mozilla/5.0 (Hausmeister-Bot/1.0 heizung-check)"}
zones = None
last_err = None
api_base = None
for base in HEIZ_API_BASES:
try:
r = requests.get(base + "/zones", timeout=8, headers=headers)
if r.status_code != 200:
last_err = f"HTTP {r.status_code}"
continue
zones = r.json()
api_base = base
break
except Exception as e: # noqa: BLE001
last_err = str(e)[:80]
if zones is None:
alerts.append(
f"🔴 Heizung: heizsteuerung API nicht erreichbar ({HEIZ_API_BASES[0]}): {last_err}"
)
return alerts
if not isinstance(zones, list):
alerts.append("🔴 Heizung: /zones liefert unerwartete Antwort")
return alerts
by_id = {z.get("id"): z for z in zones if isinstance(z, dict) and z.get("id")}
offline = sorted(
zid for zid in HEIZ_EXPECTED_ONLINE
if zid not in by_id or not by_id[zid].get("online")
)
if offline:
alerts.append(
"🔴 Heizung: TRV offline (erwartet online): " + ", ".join(offline)
)
low_batt = []
for zid in sorted(HEIZ_EXPECTED_ONLINE):
z = by_id.get(zid) or {}
if not z.get("online"):
continue
batt = z.get("battery")
if batt is None:
continue
try:
if float(batt) < HEIZ_BATTERY_WARN:
low_batt.append(f"{zid} {int(float(batt))}%")
except (TypeError, ValueError):
continue
if low_batt:
alerts.append(
f"⚠️ Heizung: Batterie <{HEIZ_BATTERY_WARN}%: " + ", ".join(low_batt)
)
# z2m Frontend — Coordinator/UI tot?
z2m_ok = False
z2m_err = None
for url in HEIZ_Z2M_URLS:
try:
r = requests.get(url, timeout=6, headers=headers, allow_redirects=True)
if r.status_code < 500:
z2m_ok = True
break
z2m_err = f"HTTP {r.status_code}"
except Exception as e: # noqa: BLE001
z2m_err = str(e)[:80]
if not z2m_ok:
alerts.append(f"🔴 Heizung: zigbee2mqtt Frontend nicht erreichbar: {z2m_err}")
# Abwesenheit laeuft, aber Backup leer = riskanter Zustand
try:
r = requests.get(api_base + "/abwesenheit", timeout=6, headers=headers)
if r.status_code == 200:
away = r.json()
if away.get("active") and not (away.get("backup_setpoints") or {}):
alerts.append(
"⚠️ Heizung: Abwesenheit AN ohne Backup-Sollwerte"
)
except Exception:
pass
return alerts
def check_hermes() -> list[str]:
"""Prueft Hermes (CT151) als kritischen Dienst ueber HTTP und Loki."""
alerts = []
@ -176,6 +288,7 @@ def check_all(state: dict | None = None) -> list[str]:
suppressed_names = config.get_suppressed_container_names(cfg)
alerts = []
alerts.extend(check_hermes())
alerts.extend(check_heizkoerper())
alerts.extend(loki_client.check_wp_mirror_sync())
containers = proxmox_client.get_all_containers(
@ -426,6 +539,18 @@ def _alert_key(alert_text: str) -> str:
m = re.search(r"([\w.\-]+\.service)", alert_text)
unit = m.group(1) if m else "generic"
return hashlib.md5(("hermes|log|" + unit).encode()).hexdigest()
if "Heizung:" in alert_text:
if "API nicht erreichbar" in alert_text:
return hashlib.md5(b"heizung|api").hexdigest()
if "zigbee2mqtt Frontend" in alert_text:
return hashlib.md5(b"heizung|z2m").hexdigest()
if "TRV offline" in alert_text:
return hashlib.md5(b"heizung|trv_offline").hexdigest()
if "Batterie" in alert_text:
return hashlib.md5(b"heizung|battery").hexdigest()
if "Abwesenheit AN ohne Backup" in alert_text:
return hashlib.md5(b"heizung|away_backup").hexdigest()
return hashlib.md5(("heizung|" + alert_text[:60]).encode()).hexdigest()
return hashlib.md5(alert_text.encode()).hexdigest()
@ -450,6 +575,8 @@ def _alert_category(alert_text: str) -> str:
return "restart"
if "Hermes" in alert_text:
return "hermes"
if "Heizung:" in alert_text:
return "heizung"
if "Backup" in alert_text or "PBS" in alert_text:
return "backup"
if "Memory läuft ab" in alert_text: