"""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, }