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:
parent
f0abeb1255
commit
03099808f0
1 changed files with 127 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ ALERT_COOLDOWN_SECONDS = {
|
||||||
"default": 3600,
|
"default": 3600,
|
||||||
"error_rate": 1800,
|
"error_rate": 1800,
|
||||||
"hermes": 7200,
|
"hermes": 7200,
|
||||||
|
"heizung": 7200,
|
||||||
"backup": 21600,
|
"backup": 21600,
|
||||||
"mirror_sync": 21600,
|
"mirror_sync": 21600,
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +103,117 @@ def _is_host_suppressed(host: str, suppressed_hosts: set) -> bool:
|
||||||
return False
|
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]:
|
def check_hermes() -> list[str]:
|
||||||
"""Prueft Hermes (CT151) als kritischen Dienst ueber HTTP und Loki."""
|
"""Prueft Hermes (CT151) als kritischen Dienst ueber HTTP und Loki."""
|
||||||
alerts = []
|
alerts = []
|
||||||
|
|
@ -176,6 +288,7 @@ def check_all(state: dict | None = None) -> list[str]:
|
||||||
suppressed_names = config.get_suppressed_container_names(cfg)
|
suppressed_names = config.get_suppressed_container_names(cfg)
|
||||||
alerts = []
|
alerts = []
|
||||||
alerts.extend(check_hermes())
|
alerts.extend(check_hermes())
|
||||||
|
alerts.extend(check_heizkoerper())
|
||||||
alerts.extend(loki_client.check_wp_mirror_sync())
|
alerts.extend(loki_client.check_wp_mirror_sync())
|
||||||
|
|
||||||
containers = proxmox_client.get_all_containers(
|
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)
|
m = re.search(r"([\w.\-]+\.service)", alert_text)
|
||||||
unit = m.group(1) if m else "generic"
|
unit = m.group(1) if m else "generic"
|
||||||
return hashlib.md5(("hermes|log|" + unit).encode()).hexdigest()
|
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()
|
return hashlib.md5(alert_text.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -450,6 +575,8 @@ def _alert_category(alert_text: str) -> str:
|
||||||
return "restart"
|
return "restart"
|
||||||
if "Hermes" in alert_text:
|
if "Hermes" in alert_text:
|
||||||
return "hermes"
|
return "hermes"
|
||||||
|
if "Heizung:" in alert_text:
|
||||||
|
return "heizung"
|
||||||
if "Backup" in alert_text or "PBS" in alert_text:
|
if "Backup" in alert_text or "PBS" in alert_text:
|
||||||
return "backup"
|
return "backup"
|
||||||
if "Memory läuft ab" in alert_text:
|
if "Memory läuft ab" in alert_text:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue