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.
370 lines
13 KiB
Python
370 lines
13 KiB
Python
"""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,
|
||
}
|