homelab-brain/homelab-ai-bot/tools/grafana.py
Homelab Cursor 5161f429df Ölknoten: Grafana-Zeile auf Heizung-Dashboard + MCP/API-Fix
Heizungs-API und Hermes-MCP liefern Ölknoten-Daten aus MQTT/sensors-Influx
statt fälschlich offline zu melden wenn nur ioBroker mqtt.0 tot ist.
2026-07-01 15:07:39 +02:00

370 lines
13 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.

"""Grafana + InfluxDB Smart-Home Tool (CT 143, pve-mu-3)."""
import requests
GRAFANA_URL = "http://100.66.78.56:3000"
GRAFANA_USER = "admin"
GRAFANA_PASS = "astral66"
INFLUX_URL = "http://100.66.78.56:8086"
INFLUX_DB_IOBROKER = "iobroker"
INFLUX_DB_SENSORS = "sensors"
# esp-satellite + Heizraum-Knoten (Telegraf → DB sensors, Measurement temperature, Tag node)
SATELLITE_NODES = {
"flur_oben": "Flur oben (Klima-Zone)",
"flur_unten": "Flur unten",
"wohnstube": "Wohnstube",
"kueche": "Küche",
"schlafzimmer": "Schlafzimmer",
"gaestezimmer": "Gästezimmer",
"trockenraum": "Trockenraum",
"oelraum": "Ölraum",
"heizraum": "Heizraum (ESP32)",
"oelknoten": "Ölknoten (Erdtrasse)",
"garage": "Garage (Test)",
}
OELKNOTEN_SENSORS = {
"raum": "Raum",
"hk_vorlauf": "HK Vorlauf",
"vl_erd_an": "Erdtrasse VL",
"rl_erd_ab": "Erdtrasse RL",
}
# Eingefrorenes ioBroker-Archiv (nur noch für Sensoren ohne esp-satellite)
LEGACY_SENSOR_MAP = {
"aussen": ("mqtt.0.Holzvergaser_Sensoren_6.Aussenfühler.temperature", "Außen"),
"garten": ("mqtt.0.ESP_Easy_schwimmbad_neu.Lufttemperatur.Lufttemperatur_Garten", "Garten/Luft"),
"schwimmbad": ("mqtt.0.ESP_Easy_schwimmbad_neu.Schwimmbad.Wassertemperatur_", "Schwimmbad"),
}
HEIZUNG_SENSOR_MAP = {
"puffer_oben": ("mqtt.0.ESP_Easy_Display_4.oben.temperature", "Puffer oben"),
"puffer_unten": ("mqtt.0.ESP_Easy_Display_4.Unten.temperature", "Puffer unten"),
"oelkessel_vl": ("mqtt.0.Oelkessel.Oelkessel_VL.Vorlauf", "Ölkessel Vorlauf"),
"brunnen_vl": ("mqtt.0.Brunnenwasser.Oelkessel_VL.Vorlauf", "Brunnenwasser Vorlauf"),
"holzvergaser_1": ("mqtt.0.Holzvergaser_Sensoren_6.temp1.temperature1", "Holzvergaser Sensor 1"),
"holzvergaser_2": ("mqtt.0.Holzvergaser_Sensoren_6.temp2.temperature2", "Holzvergaser Sensor 2"),
"aussen": LEGACY_SENSOR_MAP["aussen"],
}
ENERGIE_MEASUREMENTS = {
"pv_leistung": ("mqtt.1.openWB.pv.W", "PV Leistung"),
"pv_tagesertrag": ("mqtt.1.openWB.pv.DailyYieldKwh", "PV Tagesertrag"),
"netz_bezug": ("mqtt.1.openWB.evu.W", "Netz Bezug/Einspeisung"),
"hausverbrauch": ("mqtt.1.openWB.global.WHouseConsumption", "Hausverbrauch"),
"batterie_soc": ("mqtt.1.openWB.housebattery.%Soc", "Batterie SoC"),
"ueberschuss": ("mqtt.1.openWB.SmartHome.Status.uberschuss", "Überschuss"),
}
_ENERGIE_UNITS = {
"PV Leistung": "W",
"PV Tagesertrag": "kWh",
"Netz Bezug/Einspeisung": "W",
"Hausverbrauch": "W",
"Batterie SoC": "%",
"Überschuss": "W",
}
TOOLS = [
{
"type": "function",
"function": {
"name": "get_grafana_status",
"description": "Grafana Monitoring-Status: Health, Dashboards, Datasources, Alerts.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "get_temperaturen",
"description": (
"Aktuelle Raumtemperaturen Muldenstein (esp-satellite-Knoten via InfluxDB sensors). "
"Nutze bei Fragen zu Temperatur, Raumklima, Klimaanlage, Kühlung, Gradient zwischen Räumen."
),
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "get_energie",
"description": "Aktuelle Energiedaten: PV-Leistung, Batterie, Hausverbrauch, Netz. Nutze bei Fragen zu Solar, PV, Strom, Energie, Batterie, Wallbox, Einspeisung.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "get_heizung",
"description": "Heizungsstatus: Brenner, Puffertemperaturen, Vorlauf, Holzvergaser. Nutze bei Fragen zu Heizung, Brenner, Puffer, Holzvergaser, Ölkessel.",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
]
SYSTEM_PROMPT_EXTRA = """Für Smart-Home-Daten (Temperaturen, Energie, Heizung) im Haus Muldenstein:
- get_temperaturen: Raum-Satelliten (DB sensors, CT143 InfluxDB) — NICHT ioBroker, NICHT 192.168.178.222
- get_energie: PV, Batterie, Netz, Hausverbrauch (DB iobroker)
- get_heizung: Brenner, Puffer, Vorlauf, Holzvergaser (iobroker) + Oelknoten (sensors)
- get_grafana_status: Dashboard-Übersicht und Alerts
Datenquelle Temperaturen: Telegraf/MQTT auf CT143 (192.168.178.36), DB `sensors`.
Grafana: https://grafana.orbitalo.net/d/satelliten-temps/satelliten-temperaturen
WICHTIG: Die Tool-Ergebnisse sind bereits fertig formatiert.
Gib sie 1:1 weiter, NICHT umformulieren oder kürzen.
"""
def _api(path):
try:
r = requests.get(
f"{GRAFANA_URL}{path}",
auth=(GRAFANA_USER, GRAFANA_PASS),
timeout=10,
)
if r.ok:
return r.json()
except Exception:
pass
return None
def _influx_query(q, db=INFLUX_DB_IOBROKER):
try:
r = requests.get(
f"{INFLUX_URL}/query",
params={"db": db, "q": q},
timeout=10,
)
if r.ok:
return r.json()
except Exception:
pass
return None
def _query_latest_legacy(measurements: dict) -> list:
"""Query last() from ioBroker archive for legacy measurements."""
parts = []
for _key, (meas, label) in measurements.items():
parts.append(f'last(value) AS "{label}" FROM "{meas}"')
q = "SELECT " + "; SELECT ".join(parts) + " WHERE time > now() - 2h"
result = _influx_query(q, db=INFLUX_DB_IOBROKER)
if not result:
return []
lines = []
for stmt in result.get("results", []):
for series in stmt.get("series", []):
name = series["columns"][1] if len(series["columns"]) > 1 else series.get("name", "?")
val = series["values"][0][1] if series.get("values") else None
if val is not None:
lines.append((name, val))
return lines
def _query_satellite_temperatures() -> list:
"""Query esp-satellite nodes from DB sensors."""
q = (
"SELECT last(value) AS jetzt, min(value) AS min24h, max(value) AS max24h "
"FROM temperature WHERE time > now() - 24h GROUP BY node"
)
result = _influx_query(q, db=INFLUX_DB_SENSORS)
if not result:
return []
by_node = {}
for series in result.get("results", [{}])[0].get("series", []):
node = series.get("tags", {}).get("node")
if not node or not series.get("values"):
continue
row = series["values"][0]
by_node[node] = {
"jetzt": row[1],
"min24h": row[2],
"max24h": row[3],
}
lines = []
for node, label in SATELLITE_NODES.items():
if node not in by_node:
continue
d = by_node[node]
lines.append((label, d["jetzt"], d["min24h"], d["max24h"]))
# Unbekannte Nodes (z.B. neu) am Ende
for node, d in sorted(by_node.items()):
if node in SATELLITE_NODES:
continue
lines.append((node, d["jetzt"], d["min24h"], d["max24h"]))
return lines
def _query_oelknoten() -> dict | None:
"""Aktuelle Oelknoten-Sensoren aus DB sensors."""
q = (
"SELECT last(value) AS t FROM temperature "
"WHERE node='oelknoten' AND time > now() - 10m GROUP BY sensor"
)
result = _influx_query(q, db=INFLUX_DB_SENSORS)
if not result:
return None
temps = {}
for series in result.get("results", [{}])[0].get("series", []) or []:
sensor = series.get("tags", {}).get("sensor")
vals = series.get("values", [])
if sensor and vals and vals[0][1] is not None:
temps[sensor] = float(vals[0][1])
if not temps:
return None
st = _influx_query(
"SELECT last(value) FROM node_status WHERE node='oelknoten' AND time > now() - 10m",
db=INFLUX_DB_SENSORS,
)
online = False
try:
online = st["results"][0]["series"][0]["values"][0][1] == 1
except (KeyError, IndexError, TypeError):
online = bool(temps)
return {"online": online, "temperaturen": temps}
def handle_get_grafana_status(**kw):
health = _api("/api/health")
if not health:
return "Grafana nicht erreichbar (100.66.78.56:3000 / CT 143 pve-mu-3)"
lines = [f"Grafana v{health.get('version', '?')} — DB: {health.get('database', '?')}"]
lines.append("URL: https://grafana.orbitalo.net")
lines.append("Server: CT 143 (Raspi-Broker) auf pve-mu-3 Muldenstein")
datasources = _api("/api/datasources") or []
lines.append(f"\nDatasources: {len(datasources)}")
for ds in datasources:
lines.append(f" {ds.get('name', '?')} ({ds.get('type', '?')}) — DB: {ds.get('database', '-')}")
dashboards = _api("/api/search?type=dash-db") or []
lines.append(f"\nDashboards: {len(dashboards)}")
for db in dashboards:
url = f"https://grafana.orbitalo.net{db.get('url', '')}"
lines.append(f" {db.get('title', '?')}{url}")
alerts = _api("/api/alertmanager/grafana/api/v2/alerts") or []
if alerts:
firing = [a for a in alerts if a.get("status", {}).get("state") == "active"]
lines.append(f"\nAlerts: {len(alerts)} gesamt, {len(firing)} aktiv")
for a in firing[:5]:
labels = a.get("labels", {})
lines.append(f" {labels.get('alertname', '?')}{labels.get('severity', '?')}")
else:
lines.append("\nAlerts: keine aktiven")
return "\n".join(lines)
def handle_get_temperaturen(**kw):
satellites = _query_satellite_temperatures()
legacy = _query_latest_legacy(LEGACY_SENSOR_MAP)
if not satellites and not legacy:
return (
"Keine Temperaturdaten verfügbar.\n"
"Quelle: InfluxDB CT143 (100.66.78.56:8086), DB `sensors`.\n"
"Prüfe Telegraf/mosquitto auf CT143 — NICHT ioBroker-Host 192.168.178.222."
)
lines = ["Temperaturen Muldenstein (esp-satellite → DB sensors):"]
if satellites:
lines.append("\nRaum-Satelliten:")
for label, jetzt, min24h, max24h in satellites:
lines.append(f"- {label}: {jetzt:.1f} °C (24h: {min24h:.1f}{max24h:.1f})")
temps = [t[1] for t in satellites]
if len(temps) >= 2:
spread = max(temps) - min(temps)
coldest = min(satellites, key=lambda x: x[1])
warmest = max(satellites, key=lambda x: x[1])
lines.append(
f"\nGradient: {spread:.1f} °C "
f"({coldest[0]} {coldest[1]:.1f} °C → {warmest[0]} {warmest[1]:.1f} °C)"
)
if legacy:
lines.append("\nLegacy (DB iobroker):")
for name, val in legacy:
lines.append(f"- {name}: {val:.1f} °C")
lines.append("\nGrafana: https://grafana.orbitalo.net/d/satelliten-temps/satelliten-temperaturen")
return "\n".join(lines)
def handle_get_energie(**kw):
data = _query_latest_legacy(ENERGIE_MEASUREMENTS)
if not data:
return "Keine Energiedaten verfügbar (InfluxDB iobroker nicht erreichbar?)"
lines = ["Energie Muldenstein:"]
for name, val in data:
unit = _ENERGIE_UNITS.get(name, "")
if unit in ("%", "kWh"):
lines.append(f"- {name}: {val:.1f} {unit}")
else:
lines.append(f"- {name}: {val:.0f} {unit}")
lines.append("\nGrafana: https://grafana.orbitalo.net/d/solar/energie-uebersicht")
return "\n".join(lines)
def handle_get_heizung(**kw):
data = _query_latest_legacy(HEIZUNG_SENSOR_MAP)
brenner_q = 'SELECT last(value) FROM "brennerstatus" WHERE time > now() - 2h'
laufzeit_q = 'SELECT last(value) FROM "brenner_heute" WHERE time > now() - 2h'
starts_q = 'SELECT last(value) FROM "brennerstarts" WHERE time > now() - 24h'
lines = ["Heizung Muldenstein:"]
brenner = _influx_query(brenner_q, db=INFLUX_DB_IOBROKER)
if brenner and brenner.get("results", [{}])[0].get("series"):
val = brenner["results"][0]["series"][0]["values"][0][1]
status = "🔥 AN" if val else "⏸ AUS"
lines.append(f"\nBrenner: {status}")
laufzeit = _influx_query(laufzeit_q, db=INFLUX_DB_IOBROKER)
if laufzeit and laufzeit.get("results", [{}])[0].get("series"):
val = laufzeit["results"][0]["series"][0]["values"][0][1]
lines.append(f"Brenner heute: {val:.0f} min")
starts = _influx_query(starts_q, db=INFLUX_DB_IOBROKER)
if starts and starts.get("results", [{}])[0].get("series"):
val = starts["results"][0]["series"][0]["values"][0][1]
lines.append(f"Brennerstarts (24h): {val:.0f}")
oel = _query_oelknoten()
if oel:
status = "online" if oel["online"] else "offline"
lines.append(f"\nÖlknoten ({status}, DB sensors):")
for key, label in OELKNOTEN_SENSORS.items():
if key in oel["temperaturen"]:
lines.append(f"- {label}: {oel['temperaturen'][key]:.1f} °C")
lines.append("Grafana Ölknoten: https://grafana.orbitalo.net/d/oelknoten/olknoten")
if data:
lines.append("\nTemperaturen (Archiv iobroker):")
for name, val in data:
lines.append(f"- {name}: {val:.1f} °C")
lines.append("\nGrafana Heizung: https://grafana.orbitalo.net/d/heizung/heizung-and-puffer")
return "\n".join(lines)
HANDLERS = {
"get_grafana_status": handle_get_grafana_status,
"get_temperaturen": handle_get_temperaturen,
"get_energie": handle_get_energie,
"get_heizung": handle_get_heizung,
}