From 14e32664c8d84fed282391ec868f0104f27bf167 Mon Sep 17 00:00:00 2001 From: Homelab Cursor Date: Thu, 25 Jun 2026 10:18:20 +0200 Subject: [PATCH] fix(hausmeister): Raumtemperaturen aus InfluxDB sensors statt ioBroker-Archiv get_temperaturen liest esp-satellite-Knoten (Telegraf/DB sensors) inkl. 24h-Min/Max und Gradient. Energie/Heizung bleiben auf DB iobroker. --- homelab-ai-bot/tools/grafana.py | 179 +++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 61 deletions(-) diff --git a/homelab-ai-bot/tools/grafana.py b/homelab-ai-bot/tools/grafana.py index e5b8b8495..7020ffab0 100644 --- a/homelab-ai-bot/tools/grafana.py +++ b/homelab-ai-bot/tools/grafana.py @@ -1,27 +1,43 @@ """Grafana + InfluxDB Smart-Home Tool (CT 143, pve-mu-3).""" import requests -from urllib.parse import quote 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" +INFLUX_DB_IOBROKER = "iobroker" +INFLUX_DB_SENSORS = "sensors" -SENSOR_MAP = { +# 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)", + "garage": "Garage (Test)", +} + +# 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"), - "wohnstube": ("mqtt.0.Wohnstube_Temperatur_1.Wohnstube.Wohnstube_Temperatur", "Wohnstube"), - "kueche": ("mqtt.0.ESP_Kücheneu_2.Sensor_Küche.Küche", "Küche"), - "garage": ("mqtt.0.ESP_Easy_3.Garage_neu.Garage", "Garage"), - "puffer_oben": ("mqtt.0.ESP_Easy_Display_4.oben.temperature", "Puffer oben"), - "puffer_unten": ("mqtt.0.ESP_Easy_Display_4.Unten.temperature", "Puffer unten"), "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 = { @@ -42,8 +58,6 @@ _ENERGIE_UNITS = { "Überschuss": "W", } -_HEIZUNG_KEYS = {"puffer", "vorlauf", "holz", "brunnen", "öl"} - TOOLS = [ { "type": "function", @@ -57,7 +71,10 @@ TOOLS = [ "type": "function", "function": { "name": "get_temperaturen", - "description": "Aktuelle Temperaturen aus dem Haus in Muldenstein: Räume, Außen, Puffer, Heizung. Nutze bei Fragen zu Temperatur, Heizung, Puffer, Raumklima, 'wie warm ist es'.", + "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": []}, }, }, @@ -80,11 +97,12 @@ TOOLS = [ ] SYSTEM_PROMPT_EXTRA = """Für Smart-Home-Daten (Temperaturen, Energie, Heizung) im Haus Muldenstein: -- get_temperaturen: alle Raumtemperaturen + Außen -- get_energie: PV, Batterie, Netz, Hausverbrauch -- get_heizung: Brenner, Puffer, Vorlauf, Holzvergaser +- 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 (DB iobroker) - get_grafana_status: Dashboard-Übersicht und Alerts -Grafana-URL: https://grafana.orbitalo.net/ +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. @@ -105,11 +123,11 @@ def _api(path): return None -def _influx_query(q): +def _influx_query(q, db=INFLUX_DB_IOBROKER): try: r = requests.get( f"{INFLUX_URL}/query", - params={"db": INFLUX_DB, "q": q}, + params={"db": db, "q": q}, timeout=10, ) if r.ok: @@ -119,13 +137,13 @@ def _influx_query(q): return None -def _query_latest(measurements: dict) -> list: - """Query last() for a dict of {key: (measurement, label)}.""" +def _query_latest_legacy(measurements: dict) -> list: + """Query last() from ioBroker archive for legacy measurements.""" parts = [] - for key, (meas, label) in measurements.items(): + 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) + result = _influx_query(q, db=INFLUX_DB_IOBROKER) if not result: return [] lines = [] @@ -133,10 +151,46 @@ def _query_latest(measurements: dict) -> list: 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 - ts = series["values"][0][0] if series.get("values") else "" if val is not None: - ts_short = ts[11:16] if len(ts) > 16 else "" - lines.append((name, val, ts_short)) + 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 @@ -174,37 +228,49 @@ def handle_get_grafana_status(**kw): def handle_get_temperaturen(**kw): - data = _query_latest(SENSOR_MAP) - if not data: - return "Keine Temperaturdaten verfügbar (InfluxDB nicht erreichbar?)" + satellites = _query_satellite_temperatures() + legacy = _query_latest_legacy(LEGACY_SENSOR_MAP) - raeume = [] - heizung = [] - for name, val, ts in data: - entry = f"- {name}: {val:.1f} °C" - if any(k in name.lower() for k in _HEIZUNG_KEYS): - heizung.append(entry) - else: - raeume.append(entry) + 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:"] - if raeume: - lines.append("\nRäume & Außen:") - lines.extend(raeume) - if heizung: - lines.append("\nHeizung & Puffer:") - lines.extend(heizung) - lines.append("\nGrafana: https://grafana.orbitalo.net/d/solar/raumtemperaturen") + 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(ENERGIE_MEASUREMENTS) + data = _query_latest_legacy(ENERGIE_MEASUREMENTS) if not data: - return "Keine Energiedaten verfügbar (InfluxDB nicht erreichbar?)" + return "Keine Energiedaten verfügbar (InfluxDB iobroker nicht erreichbar?)" lines = ["Energie Muldenstein:"] - for name, val, ts in data: + for name, val in data: unit = _ENERGIE_UNITS.get(name, "") if unit in ("%", "kWh"): lines.append(f"- {name}: {val:.1f} {unit}") @@ -216,16 +282,7 @@ def handle_get_energie(**kw): def handle_get_heizung(**kw): - heiz_sensors = { - "puffer_oben": SENSOR_MAP["puffer_oben"], - "puffer_unten": SENSOR_MAP["puffer_unten"], - "oelkessel_vl": SENSOR_MAP["oelkessel_vl"], - "brunnen_vl": SENSOR_MAP["brunnen_vl"], - "holzvergaser_1": SENSOR_MAP["holzvergaser_1"], - "holzvergaser_2": SENSOR_MAP["holzvergaser_2"], - "aussen": SENSOR_MAP["aussen"], - } - data = _query_latest(heiz_sensors) + 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' @@ -233,25 +290,25 @@ def handle_get_heizung(**kw): lines = ["Heizung Muldenstein:"] - brenner = _influx_query(brenner_q) + 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) + 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) + 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}") if data: - lines.append("\nTemperaturen:") - for name, val, ts in data: + lines.append("\nTemperaturen (Archiv iobroker):") + for name, val in data: lines.append(f"- {name}: {val:.1f} °C") lines.append("\nGrafana: https://grafana.orbitalo.net/d/heizung/heizung-and-puffer")