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)."""
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")