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.
This commit is contained in:
Homelab Cursor 2026-06-25 10:18:20 +02:00
parent b4bb0b1a82
commit 14e32664c8

View file

@ -1,27 +1,43 @@
"""Grafana + InfluxDB Smart-Home Tool (CT 143, pve-mu-3).""" """Grafana + InfluxDB Smart-Home Tool (CT 143, pve-mu-3)."""
import requests import requests
from urllib.parse import quote
GRAFANA_URL = "http://100.66.78.56:3000" GRAFANA_URL = "http://100.66.78.56:3000"
GRAFANA_USER = "admin" GRAFANA_USER = "admin"
GRAFANA_PASS = "astral66" GRAFANA_PASS = "astral66"
INFLUX_URL = "http://100.66.78.56:8086" 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"), "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"), "garten": ("mqtt.0.ESP_Easy_schwimmbad_neu.Lufttemperatur.Lufttemperatur_Garten", "Garten/Luft"),
"schwimmbad": ("mqtt.0.ESP_Easy_schwimmbad_neu.Schwimmbad.Wassertemperatur_", "Schwimmbad"), "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"), "oelkessel_vl": ("mqtt.0.Oelkessel.Oelkessel_VL.Vorlauf", "Ölkessel Vorlauf"),
"brunnen_vl": ("mqtt.0.Brunnenwasser.Oelkessel_VL.Vorlauf", "Brunnenwasser Vorlauf"), "brunnen_vl": ("mqtt.0.Brunnenwasser.Oelkessel_VL.Vorlauf", "Brunnenwasser Vorlauf"),
"holzvergaser_1": ("mqtt.0.Holzvergaser_Sensoren_6.temp1.temperature1", "Holzvergaser Sensor 1"), "holzvergaser_1": ("mqtt.0.Holzvergaser_Sensoren_6.temp1.temperature1", "Holzvergaser Sensor 1"),
"holzvergaser_2": ("mqtt.0.Holzvergaser_Sensoren_6.temp2.temperature2", "Holzvergaser Sensor 2"), "holzvergaser_2": ("mqtt.0.Holzvergaser_Sensoren_6.temp2.temperature2", "Holzvergaser Sensor 2"),
"aussen": LEGACY_SENSOR_MAP["aussen"],
} }
ENERGIE_MEASUREMENTS = { ENERGIE_MEASUREMENTS = {
@ -42,8 +58,6 @@ _ENERGIE_UNITS = {
"Überschuss": "W", "Überschuss": "W",
} }
_HEIZUNG_KEYS = {"puffer", "vorlauf", "holz", "brunnen", "öl"}
TOOLS = [ TOOLS = [
{ {
"type": "function", "type": "function",
@ -57,7 +71,10 @@ TOOLS = [
"type": "function", "type": "function",
"function": { "function": {
"name": "get_temperaturen", "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": []}, "parameters": {"type": "object", "properties": {}, "required": []},
}, },
}, },
@ -80,11 +97,12 @@ TOOLS = [
] ]
SYSTEM_PROMPT_EXTRA = """Für Smart-Home-Daten (Temperaturen, Energie, Heizung) im Haus Muldenstein: SYSTEM_PROMPT_EXTRA = """Für Smart-Home-Daten (Temperaturen, Energie, Heizung) im Haus Muldenstein:
- get_temperaturen: alle Raumtemperaturen + Außen - get_temperaturen: Raum-Satelliten (DB sensors, CT143 InfluxDB) NICHT ioBroker, NICHT 192.168.178.222
- get_energie: PV, Batterie, Netz, Hausverbrauch - get_energie: PV, Batterie, Netz, Hausverbrauch (DB iobroker)
- get_heizung: Brenner, Puffer, Vorlauf, Holzvergaser - get_heizung: Brenner, Puffer, Vorlauf, Holzvergaser (DB iobroker)
- get_grafana_status: Dashboard-Übersicht und Alerts - 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. WICHTIG: Die Tool-Ergebnisse sind bereits fertig formatiert.
Gib sie 1:1 weiter, NICHT umformulieren oder kürzen. Gib sie 1:1 weiter, NICHT umformulieren oder kürzen.
@ -105,11 +123,11 @@ def _api(path):
return None return None
def _influx_query(q): def _influx_query(q, db=INFLUX_DB_IOBROKER):
try: try:
r = requests.get( r = requests.get(
f"{INFLUX_URL}/query", f"{INFLUX_URL}/query",
params={"db": INFLUX_DB, "q": q}, params={"db": db, "q": q},
timeout=10, timeout=10,
) )
if r.ok: if r.ok:
@ -119,13 +137,13 @@ def _influx_query(q):
return None return None
def _query_latest(measurements: dict) -> list: def _query_latest_legacy(measurements: dict) -> list:
"""Query last() for a dict of {key: (measurement, label)}.""" """Query last() from ioBroker archive for legacy measurements."""
parts = [] parts = []
for key, (meas, label) in measurements.items(): for _key, (meas, label) in measurements.items():
parts.append(f'last(value) AS "{label}" FROM "{meas}"') parts.append(f'last(value) AS "{label}" FROM "{meas}"')
q = "SELECT " + "; SELECT ".join(parts) + " WHERE time > now() - 2h" q = "SELECT " + "; SELECT ".join(parts) + " WHERE time > now() - 2h"
result = _influx_query(q) result = _influx_query(q, db=INFLUX_DB_IOBROKER)
if not result: if not result:
return [] return []
lines = [] lines = []
@ -133,10 +151,46 @@ def _query_latest(measurements: dict) -> list:
for series in stmt.get("series", []): for series in stmt.get("series", []):
name = series["columns"][1] if len(series["columns"]) > 1 else series.get("name", "?") name = series["columns"][1] if len(series["columns"]) > 1 else series.get("name", "?")
val = series["values"][0][1] if series.get("values") else None 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: if val is not None:
ts_short = ts[11:16] if len(ts) > 16 else "" lines.append((name, val))
lines.append((name, val, ts_short)) 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 return lines
@ -174,37 +228,49 @@ def handle_get_grafana_status(**kw):
def handle_get_temperaturen(**kw): def handle_get_temperaturen(**kw):
data = _query_latest(SENSOR_MAP) satellites = _query_satellite_temperatures()
if not data: legacy = _query_latest_legacy(LEGACY_SENSOR_MAP)
return "Keine Temperaturdaten verfügbar (InfluxDB nicht erreichbar?)"
raeume = [] if not satellites and not legacy:
heizung = [] return (
for name, val, ts in data: "Keine Temperaturdaten verfügbar.\n"
entry = f"- {name}: {val:.1f} °C" "Quelle: InfluxDB CT143 (100.66.78.56:8086), DB `sensors`.\n"
if any(k in name.lower() for k in _HEIZUNG_KEYS): "Prüfe Telegraf/mosquitto auf CT143 — NICHT ioBroker-Host 192.168.178.222."
heizung.append(entry) )
else:
raeume.append(entry)
lines = ["Temperaturen Muldenstein:"] lines = ["Temperaturen Muldenstein (esp-satellite → DB sensors):"]
if raeume:
lines.append("\nRäume & Außen:") if satellites:
lines.extend(raeume) lines.append("\nRaum-Satelliten:")
if heizung: for label, jetzt, min24h, max24h in satellites:
lines.append("\nHeizung & Puffer:") lines.append(f"- {label}: {jetzt:.1f} °C (24h: {min24h:.1f}{max24h:.1f})")
lines.extend(heizung)
lines.append("\nGrafana: https://grafana.orbitalo.net/d/solar/raumtemperaturen") 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) return "\n".join(lines)
def handle_get_energie(**kw): def handle_get_energie(**kw):
data = _query_latest(ENERGIE_MEASUREMENTS) data = _query_latest_legacy(ENERGIE_MEASUREMENTS)
if not data: if not data:
return "Keine Energiedaten verfügbar (InfluxDB nicht erreichbar?)" return "Keine Energiedaten verfügbar (InfluxDB iobroker nicht erreichbar?)"
lines = ["Energie Muldenstein:"] lines = ["Energie Muldenstein:"]
for name, val, ts in data: for name, val in data:
unit = _ENERGIE_UNITS.get(name, "") unit = _ENERGIE_UNITS.get(name, "")
if unit in ("%", "kWh"): if unit in ("%", "kWh"):
lines.append(f"- {name}: {val:.1f} {unit}") lines.append(f"- {name}: {val:.1f} {unit}")
@ -216,16 +282,7 @@ def handle_get_energie(**kw):
def handle_get_heizung(**kw): def handle_get_heizung(**kw):
heiz_sensors = { data = _query_latest_legacy(HEIZUNG_SENSOR_MAP)
"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)
brenner_q = 'SELECT last(value) FROM "brennerstatus" WHERE time > now() - 2h' brenner_q = 'SELECT last(value) FROM "brennerstatus" WHERE time > now() - 2h'
laufzeit_q = 'SELECT last(value) FROM "brenner_heute" 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:"] 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"): if brenner and brenner.get("results", [{}])[0].get("series"):
val = brenner["results"][0]["series"][0]["values"][0][1] val = brenner["results"][0]["series"][0]["values"][0][1]
status = "🔥 AN" if val else "⏸ AUS" status = "🔥 AN" if val else "⏸ AUS"
lines.append(f"\nBrenner: {status}") 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"): if laufzeit and laufzeit.get("results", [{}])[0].get("series"):
val = laufzeit["results"][0]["series"][0]["values"][0][1] val = laufzeit["results"][0]["series"][0]["values"][0][1]
lines.append(f"Brenner heute: {val:.0f} min") 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"): if starts and starts.get("results", [{}])[0].get("series"):
val = starts["results"][0]["series"][0]["values"][0][1] val = starts["results"][0]["series"][0]["values"][0][1]
lines.append(f"Brennerstarts (24h): {val:.0f}") lines.append(f"Brennerstarts (24h): {val:.0f}")
if data: if data:
lines.append("\nTemperaturen:") lines.append("\nTemperaturen (Archiv iobroker):")
for name, val, ts in data: for name, val in data:
lines.append(f"- {name}: {val:.1f} °C") lines.append(f"- {name}: {val:.1f} °C")
lines.append("\nGrafana: https://grafana.orbitalo.net/d/heizung/heizung-and-puffer") lines.append("\nGrafana: https://grafana.orbitalo.net/d/heizung/heizung-and-puffer")