Garage-Decke Doku und Hermes-MCP ins Git versionieren.
Projektdoku, MCP-Skript und Deploy-Hinweise als Quelle der Wahrheit in homelab-brain; CT999 und Telegraf-README verlinken darauf.
This commit is contained in:
parent
e64eed53e1
commit
48852c8665
5 changed files with 681 additions and 1 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
| **Arakava News** | arakava-news/STATE.md | WordPress + RSS-Manager + KI-Artikel |
|
| **Arakava News** | arakava-news/STATE.md | WordPress + RSS-Manager + KI-Artikel |
|
||||||
| **Edelmetall Dashboard** | edelmetall/STATE.md | Gold/Silber Preisbot |
|
| **Edelmetall Dashboard** | edelmetall/STATE.md | Gold/Silber Preisbot |
|
||||||
| **Smart Home** | smart-home/STATE.md | ioBroker + InfluxDB + Grafana (alle in CT 143 pve-mu-3) |
|
| **Smart Home** | smart-home/STATE.md | ioBroker + InfluxDB + Grafana (alle in CT 143 pve-mu-3) |
|
||||||
|
| **Garage Decke Brunnenkühlung** | docs/projekte/garage-decke-monitoring.md | 5× DS18B20, Shelly, Grafana, Hermes MCP |
|
||||||
| **Heizung & Ölverbrauch** | smart-home/HEIZUNG.md | Brennererkennung, Dashboard, Zugänge, Scripts |
|
| **Heizung & Ölverbrauch** | smart-home/HEIZUNG.md | Brennererkennung, Dashboard, Zugänge, Scripts |
|
||||||
| **ESP32 Projekte** | esp32/PLAN.md | Heizungssteuerung, Sensorik |
|
| **ESP32 Projekte** | esp32/PLAN.md | Heizungssteuerung, Sensorik |
|
||||||
| **FünfVorAcht** | fuenfvoracht/STATE.md | Telegram KI-Poster (täglich 19:55) |
|
| **FünfVorAcht** | fuenfvoracht/STATE.md | Telegram KI-Poster (täglich 19:55) |
|
||||||
|
|
|
||||||
341
docs/projekte/garage-decke-monitoring.md
Normal file
341
docs/projekte/garage-decke-monitoring.md
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
# Garage-Decke — Deckentemperaturen (Klimadecke, Brunnen/PWT)
|
||||||
|
|
||||||
|
**Stand:** 27.06.2026 (Abend) · **Datendrehscheibe:** CT 143 (Raspi-Broker) · **Grafana:** https://grafana.orbitalo.net/d/garage_decke/garage-decke
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Fuenf DS18B20-Fuehler an der Garage-Decke **dauerhaft messen und auswerten**:
|
||||||
|
|
||||||
|
- **Klimadecke:** Vorlauf + Ruecklauf (VL/RL) — passive/strahlungsbasierte Kuehlung
|
||||||
|
- **Brunnen/PWT:** Vorlauf vor Plattenwaermetauscher + Ruecklauf danach
|
||||||
|
- **PWT-Oberflaeche:** Fuehler oben auf PWT geklemmt (Gehaeuse, schneller Indikator)
|
||||||
|
|
||||||
|
Aus den Temperaturen werden **ΔT (Effektivitaet)** und — nur fuer den Brunnenkreis — **thermische Leistung** abgeleitet.
|
||||||
|
|
||||||
|
Getrennt vom **Garagen-Raum-Satelliten** (`garage/temp`, ESP-Werkstattknoten) — der misst Raumluft, nicht Deckenrohre.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
Brunnen (Garten)
|
||||||
|
│ Brunnenpumpe + ggf. Verstaerkerpumpe(n) in Reihe
|
||||||
|
│ lange Leitung → Garage
|
||||||
|
▼
|
||||||
|
Plattenwaermetauscher (PWT) — Primaer: Brunnen / Sekundaer: KaelteDecke
|
||||||
|
│
|
||||||
|
2x Shelly 1 (Gen1, SHSW-1) + DS18B20-Addon
|
||||||
|
│ HTTP lokal: 192.168.178.86 / .78
|
||||||
|
│ MQTT (Gen1: Cloud ODER MQTT, nicht beides!)
|
||||||
|
│ Topics: shellies/<id>/ext_temperature/<idx> (float °C)
|
||||||
|
▼
|
||||||
|
mosquitto CT143 :1883 (192.168.178.36)
|
||||||
|
▼
|
||||||
|
Telegraf satellites.conf (5 Inputs, feste Tags)
|
||||||
|
▼
|
||||||
|
InfluxDB DB `sensors`, Measurement `temperature`
|
||||||
|
Tags: node=garage_decke, sensor=<rolle>
|
||||||
|
▼
|
||||||
|
Grafana Dashboard „Garage Decke“ (UID: garage_decke)
|
||||||
|
→ Stat: Temperaturen, ΔT, Leistung Brunnen
|
||||||
|
→ Verlauf: 5 Fuehler + ΔT/Leistung
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code / Git (homelab-brain auf pve-hetzner)
|
||||||
|
|
||||||
|
| Artefakt | Pfad im Repo | Deploy-Ziel |
|
||||||
|
|----------|--------------|-------------|
|
||||||
|
| Telegraf MQTT-Inputs | `infra/telegraf/satellites.conf` | CT143 `/etc/telegraf/telegraf.d/satellites.conf` |
|
||||||
|
| Grafana Dashboard JSON | `infra/grafana/garage_decke.json` | CT143 `/etc/grafana/provisioning/dashboards/garage_decke.json` |
|
||||||
|
| Hermes MCP | `infra/hermes/garage_decke_mcp.py` | CT151 `/root/.hermes/garage_decke_mcp.py` |
|
||||||
|
| Projektdoku | `docs/projekte/garage-decke-monitoring.md` | CT999 `/root/docs/projekte/garage-decke-monitoring.md` |
|
||||||
|
|
||||||
|
Nach Aenderung: **sofort `git push origin main`**, sonst ueberschreibt Auto-Sync (`sync-state.sh` alle 15 min).
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
### Sensoren (Shelly)
|
||||||
|
|
||||||
|
| Geraet | IP | Shelly-ID | MQTT-Prefix | Fuehler |
|
||||||
|
|--------|-----|-----------|-------------|---------|
|
||||||
|
| Shelly 1 | 192.168.178.86 | shelly1-F4CFA2D3B444 | `shellies/shelly1-F4CFA2D3B444` | 3x DS18B20 (idx 0–2) |
|
||||||
|
| Shelly 1 | 192.168.178.78 | shelly1-8CAAB55D9A15 | `shellies/shelly1-8CAAB55D9A15` | 2x DS18B20 (idx 0–1) |
|
||||||
|
|
||||||
|
### Sensor-Zuordnung (final 26.06.2026)
|
||||||
|
|
||||||
|
| InfluxDB `sensor` | Bedeutung | Shelly | idx | hwID |
|
||||||
|
|-------------------|-----------|--------|-----|------|
|
||||||
|
| `rl_klimadecke` | RL Klimadecke (Ruecklauf, waermer) | .86 | 0 | 28ff641e01be641a |
|
||||||
|
| `pwt` | PWT-Oberflaeche (Gehaeuse oben) | .86 | 1 | 28ff641e01b534e2 |
|
||||||
|
| `vl_kaeltedecke` | VL KaelteDecke (Vorlauf, kalt) | .86 | 2 | 28ff641e23c9d2f6 |
|
||||||
|
| `vl_brunnen` | VL Brunnen (vor PWT, kalt) | .78 | 0 | 28ff641e23ea0235 |
|
||||||
|
| `rl_brunnen` | RL Brunnen (nach PWT, waermer) | .78 | 1 | 28ff641e23f7232d |
|
||||||
|
|
||||||
|
**Wichtig:** InfluxDB-Tag `node=garage_decke` — **nicht** `garage` (das ist der Raum-Satellit).
|
||||||
|
|
||||||
|
Urspruenglich `decke_1..5` / `reserve` — umbenannt in semantische Tags; alte `last()`-Werte in Influx ignorieren.
|
||||||
|
|
||||||
|
### Hydraulik / Pumpen (Stand 27.06.2026 Abend)
|
||||||
|
|
||||||
|
| Komponente | Details |
|
||||||
|
|------------|---------|
|
||||||
|
| **Brunnenpumpe** | Bestehend, liefert Volumen am Brunnen |
|
||||||
|
| **KD-Pumpe** | 2. Pumpe KaelteDecke; Flussrichtung umgedreht (Entlueftung) → Grafana `kd_flussrichtung = Umgekehrt` |
|
||||||
|
| **Verstaerkerpumpe** | **ZC-T50** (haendlerisch „50 W“), Typenschild: **24 V DC, 3,5 A ≈ 84 W** elektrisch; als Druckerhoehungspumpe |
|
||||||
|
| **Aktueller Test** | **3 Pumpen in Reihe** (Brunnen + Verstaerker + KD) — Durchfluss **5,27 l/min** gemessen (27.06. Abend) |
|
||||||
|
| **Mittelfristig** | **1–2 Pumpen ausbauen** — minimale Kombination finden, bei der l/min + PWT/KD-ΔT gleich bleiben |
|
||||||
|
|
||||||
|
**Erkenntnis Garten vs. Garage:** Am Brunnen direkt wirkt die Zusatzpumpe stark; in der Garage weniger. Engpass ist **Druckverlust** (lange Leitung + PWT + KaelteDecke), nicht zu wenig Brunnenwasser. Mehr Pumpen in Reihe kompensieren das kurzfristig; dauerhaft besser: **eine** gut platzierte Verstaerkerpumpe (Zulauf beachten, Kavitation vermeiden).
|
||||||
|
|
||||||
|
**Nach jedem Pumpen-Umbau:** entlueften, 30–60 min stabilisieren, Durchfluss mit Wasseruhr messen (gleiche Stelle wie Benchmark), Grafana-Variable `brunnen_durchfluss` setzen.
|
||||||
|
|
||||||
|
### KD-Flussrichtung optimiert (27.06.2026 — zentraler Befund)
|
||||||
|
|
||||||
|
**Problem vor Umkehr:** Nur ca. **20 % der KaelteDecke** wurde spuerbar kalt — Rest stagnierte oder wurde nicht durchstroemt (Hydraulik/Entlueftung, nicht Sensorfehler).
|
||||||
|
|
||||||
|
**Massnahme:** Durchfluss in der **KaelteDecke** optimiert durch **Umkehr der Flussrichtung** (2. KD-Pumpe, Entlueftung). Grafana: `kd_flussrichtung = Umgekehrt` (-1).
|
||||||
|
|
||||||
|
**Ergebnis:** Erst **nach** der Umkehr wurde die **gesamte** KaelteDecke kalt. Daten bestaetigen den Sprung (~09:30 Uhr):
|
||||||
|
|
||||||
|
| Phase | Hydraulisches VL KD | Hydraulisches RL KD | ΔT KD | Wirkung |
|
||||||
|
|-------|---------------------|---------------------|-------|---------|
|
||||||
|
| Frueh (wenig Durchfluss) | ~27 °C | ~20 °C | ~0 K (VL waermer als RL) | Decke praktisch aus |
|
||||||
|
| Nach Flussumkehr + Pumpen | **~20 °C** ↓ | **~25 °C** ↑ | **~5,5 K** | volle Decke kalt |
|
||||||
|
|
||||||
|
**Interpretation:** Fuehler bleiben am Rohr fix montiert — die **hydraulische** VL/RL-Zuordnung aendert sich durch die Flussrichtung. Vorher lief vermutlich nur ein Kurzschluss-/Teilstrom (ein Strang kalt, Rest tot). Nach Umkehr: Durchstroemung der gesamten Flaeche, VL faellt deutlich, RL steigt — typisches Kuehlbetriebsbild.
|
||||||
|
|
||||||
|
**Fuer kuenftige Wartung:** Flussrichtung und Entlueftung sind genauso wichtig wie Pumpenleistung. Ohne korrekte Richtung liefert mehr Druck allein keine volle Decke.
|
||||||
|
|
||||||
|
### Montage Fuehler
|
||||||
|
|
||||||
|
| Fuehler | Status | Anmerkung |
|
||||||
|
|---------|--------|-----------|
|
||||||
|
| Klimadecke VL/RL | montiert, plausibel | ΔT ~4–6 K je nach Hydraulik |
|
||||||
|
| PWT-Oberflaeche | auf PWT geklemmt | reagiert schnell; Indikator „PWT wird kalt“ |
|
||||||
|
| Brunnen VL/RL | isoliert gegen Raumluft (27.06.) | ΔT ~2,4–2,8 K; optional Tauchhuelle nachruesten |
|
||||||
|
|
||||||
|
### Shelly Gen1: MQTT statt App
|
||||||
|
|
||||||
|
Shelly 1 (Gen1) kann **entweder Cloud (Shelly-App) oder MQTT** — nicht parallel. Fuer Monitoring ist MQTT aktiv:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<ip>/settings/mqtt?mqtt_enable=true&mqtt_server=192.168.178.36:1883&mqtt_clean_session=true&mqtt_update_period=30
|
||||||
|
```
|
||||||
|
|
||||||
|
Lokale Werte/Identifikation: Browser `http://192.168.178.86` bzw. `.78/status` → `ext_temperature`.
|
||||||
|
|
||||||
|
## MQTT-Topics (Beispiel)
|
||||||
|
|
||||||
|
```
|
||||||
|
shellies/shelly1-F4CFA2D3B444/ext_temperature/0 → 29.5
|
||||||
|
shellies/shelly1-F4CFA2D3B444/ext_temperatures → JSON mit hwID + tC
|
||||||
|
shellies/shelly1-8CAAB55D9A15/ext_temperature/1 → 22.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Telegraf / InfluxDB
|
||||||
|
|
||||||
|
Fuenf feste `[[inputs.mqtt_consumer]]`-Bloecke in `satellites.conf` — je Topic ein Input mit festen Tags `node=garage_decke`, `sensor=<rolle>`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Auszug infra/telegraf/satellites.conf (Kommentarblock + Tags)
|
||||||
|
# rl_klimadecke = shelly .86 idx0
|
||||||
|
# pwt = shelly .86 idx1
|
||||||
|
# vl_kaeltedecke = shelly .86 idx2
|
||||||
|
# vl_brunnen = shelly .78 idx0
|
||||||
|
# rl_brunnen = shelly .78 idx1
|
||||||
|
|
||||||
|
[[inputs.mqtt_consumer]]
|
||||||
|
topics = ["shellies/shelly1-F4CFA2D3B444/ext_temperature/0"]
|
||||||
|
name_override = "temperature"
|
||||||
|
[inputs.mqtt_consumer.tags]
|
||||||
|
node = "garage_decke"
|
||||||
|
sensor = "rl_klimadecke"
|
||||||
|
# ... analog fuer pwt, vl_kaeltedecke, vl_brunnen, rl_brunnen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verifikation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MQTT live (auf CT143)
|
||||||
|
mosquitto_sub -h 127.0.0.1 -t 'shellies/shelly1-F4CFA2D3B444/ext_temperature/#' -v
|
||||||
|
|
||||||
|
# InfluxDB
|
||||||
|
curl -sG 'http://127.0.0.1:8086/query' --data-urlencode 'db=sensors' \
|
||||||
|
--data-urlencode "q=SELECT last(\"value\") FROM \"temperature\" WHERE \"node\"='garage_decke' GROUP BY \"sensor\""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benchmark / Messwerte
|
||||||
|
|
||||||
|
### Durchfluss
|
||||||
|
|
||||||
|
| Datum | Konfiguration | l/min | Quelle |
|
||||||
|
|-------|---------------|-------|--------|
|
||||||
|
| 27.06.2026 | Brunnenpumpe + KD (ohne Verstaerker) | **3,5** | Wasseruhr |
|
||||||
|
| 27.06.2026 Abend | + ZC-T50, 3 Pumpen in Reihe | **5,27** | Wasseruhr — **Grafana-Default** |
|
||||||
|
|
||||||
|
### Temperatur-Vergleich (kd_flussrichtung = Umgekehrt)
|
||||||
|
|
||||||
|
| Kennzahl | Vor Verstaerkerpumpe | Nach Verstaerkerpumpe (27.06. Abend) |
|
||||||
|
|----------|----------------------|--------------------------------------|
|
||||||
|
| VL Brunnen | 15,8 °C | **15,1 °C** |
|
||||||
|
| RL Brunnen | 18,5 °C | **17,6 °C** |
|
||||||
|
| **ΔT Brunnen** | 2,7 K | **2,4–2,5 K** |
|
||||||
|
| PWT-Oberflaeche | 17,7 °C | **16,2 °C** |
|
||||||
|
| KD-ΔT (RL−VL) | ~4,5 K | **~6,0 K** |
|
||||||
|
| Leistung Brunnen | **~659–684 W** @ 3,5 l/min, ΔT ~2,7 K | **~772 W** @ 5,27 l/min, ΔT ~2,1 K (live); **~920 W** bei ΔT 2,5 K |
|
||||||
|
| Garage-Raum (`node=garage`) | ~27,4 °C | ~27,8 °C (Traegheit; Decke fuehlt sich kaelter an) |
|
||||||
|
|
||||||
|
**Interpretation:** Sekundaerseite (PWT, KD) profitiert deutlich. Brunnen-ΔT sinkt leicht (typisch bei mehr Volumen). **Netto-Leistung** erst nach neuem Durchfluss: `Q [W] ≈ l/min × ΔT × 69,8`.
|
||||||
|
|
||||||
|
**Beispielrechnung bei ΔT = 2,5 K:**
|
||||||
|
|
||||||
|
| l/min | ~Leistung |
|
||||||
|
|-------|-----------|
|
||||||
|
| 5,0 | ~837 W |
|
||||||
|
| 6,0 | ~1005 W |
|
||||||
|
| 7,0 | ~1172 W |
|
||||||
|
|
||||||
|
## Grafana
|
||||||
|
|
||||||
|
| Eigenschaft | Wert |
|
||||||
|
|-------------|------|
|
||||||
|
| Titel | Garage Decke |
|
||||||
|
| UID | `garage_decke` |
|
||||||
|
| URL | `/d/garage_decke/garage-decke` |
|
||||||
|
| Datasource | InfluxDB-sensors (DB `sensors`), Math-Panels: **Mixed** |
|
||||||
|
| Refresh | 30 s |
|
||||||
|
| Stat-Anzeige | Hintergrund **gruen** (OK) oder **schwarz** (kein Signal) |
|
||||||
|
|
||||||
|
### Dashboard-Variablen
|
||||||
|
|
||||||
|
| Variable | Typ | Default | Zweck |
|
||||||
|
|----------|-----|---------|-------|
|
||||||
|
| `kd_flussrichtung` | Dropdown `1` / `-1` | **-1** (Umgekehrt) | Tauscht hydraulische VL/RL-Zuordnung der fest montierten KD-Fuehler |
|
||||||
|
| `brunnen_durchfluss` | Dropdown 3 … 8 l/min (inkl. **5,27**) | **5,27** | Brunnen-Volumenstrom fuer Leistungsberechnung |
|
||||||
|
|
||||||
|
Durchfluss nur **Brunnenkreis** bekannt. KD-Kreis: nur ΔT, keine kW ohne Durchflussmesser.
|
||||||
|
|
||||||
|
### KD Flussrichtung (`kd_flussrichtung`)
|
||||||
|
|
||||||
|
Fuehler bleiben am Rohr (`rl_klimadecke` idx0, `vl_kaeltedecke` idx2). Math in `garage_decke.json`:
|
||||||
|
|
||||||
|
```
|
||||||
|
RL_anzeige = (A + B - kd_flussrichtung * (B - A)) / 2
|
||||||
|
VL_anzeige = (A + B + kd_flussrichtung * (B - A)) / 2
|
||||||
|
ΔT_KD = RL_anzeige - VL_anzeige
|
||||||
|
```
|
||||||
|
|
||||||
|
(A = `rl_klimadecke`, B = `vl_kaeltedecke` in Influx)
|
||||||
|
|
||||||
|
### Panels
|
||||||
|
|
||||||
|
**Zeile 1 — Temperaturen:** RL KD, PWT, VL KD, VL Brunnen, RL Brunnen
|
||||||
|
|
||||||
|
**Zeile 2 — Berechnet:**
|
||||||
|
|
||||||
|
| Panel | Formel | Einheit |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| ΔT KaelteDecke | RL − VL (per Flussrichtung) | K |
|
||||||
|
| ΔT Brunnen | `rl_brunnen − vl_brunnen` | K |
|
||||||
|
| Leistung Brunnen | `brunnen_durchfluss × ΔT_Brunnen × 0,0698` | kW (Anzeige W) |
|
||||||
|
|
||||||
|
**Verlaufs-Charts:** 5 Temperaturen; ΔT KD + ΔT Brunnen + Leistung Brunnen
|
||||||
|
|
||||||
|
### Formel Leistung Brunnen
|
||||||
|
|
||||||
|
```
|
||||||
|
Q [kW] ≈ V̇ [l/min] × ΔT [K] × 0,0698
|
||||||
|
Q [W] ≈ V̇ [l/min] × ΔT [K] × 69,8
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benchmark ohne Verstaerker (27.06.):** 3,5 l/min × 2,8 K × 69,8 ≈ **684 W**
|
||||||
|
|
||||||
|
**Benchmark mit Verstaerker (27.06. Abend):** 5,27 l/min × 2,1 K × 69,8 ≈ **772 W** (gemessen); bei ΔT 2,5 K ≈ **920 W**
|
||||||
|
|
||||||
|
### Grafana — technische Fixes (27.06.2026)
|
||||||
|
|
||||||
|
| Problem | Ursache | Fix (Commit) |
|
||||||
|
|---------|---------|--------------|
|
||||||
|
| 5× No Data in Stat-Panels | `filterByRefId` filterte Math-Abhaengigkeiten A/B weg | `hide: true` auf A/B, kein filterByRefId |
|
||||||
|
| No Data bei Math | Panel-DS Influx statt Mixed | Datasource `-- Mixed --` |
|
||||||
|
| Rot/Gruen-Flackern | `colorMode: background` + mehrere Serien | Nur eine sichtbare Serie; zweistufige Thresholds |
|
||||||
|
| KD-ΔT negativ | Swap-Formel (B−A) statt (B−A) mit Richtung | `kd_flussrichtung` + korrigierte Math |
|
||||||
|
| PWT-ΔT-Panel | wenig Aussagekraft | durch **ΔT Brunnen** ersetzt |
|
||||||
|
|
||||||
|
### Grenzen
|
||||||
|
|
||||||
|
- **KD-Leistung kW:** ohne Durchflussmesser im KD-Kreis nicht berechenbar
|
||||||
|
- **Garagen-Kuehlleistung gesamt:** Raumtemp `garage` vs. `aussen` — separates Orakel
|
||||||
|
- **Brunnen-Leistung:** abhaengig von Wasseruhr-Messung und VL/RL-Montage
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sync auf pve-hetzner AUS, dann in /root/homelab-brain/ editieren, commit + push
|
||||||
|
|
||||||
|
# auf pve-mu-3:
|
||||||
|
pct push 143 satellites.conf /etc/telegraf/telegraf.d/satellites.conf
|
||||||
|
pct exec 143 -- systemctl restart telegraf
|
||||||
|
|
||||||
|
pct push 143 garage_decke.json /etc/grafana/provisioning/dashboards/garage_decke.json
|
||||||
|
# Optional: API-Overwrite fuer sofortige Live-Aenderung (siehe Session 27.06.)
|
||||||
|
pct exec 143 -- systemctl restart grafana-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwandte Messpunkte
|
||||||
|
|
||||||
|
| Quelle | node | Zweck |
|
||||||
|
|--------|------|-------|
|
||||||
|
| Garagen-Raum-Satellit | `garage` | Raumluft Werkstatt |
|
||||||
|
| Aussenfuehler ESP8266 | `aussen` | Temp. unter Dach/Vordach (MQTT legacy: `oelraum/temp`) |
|
||||||
|
| Oelknoten | `oelknoten` | Oelraum-Temperaturen (separates Board `oelknoten`) |
|
||||||
|
|
||||||
|
## Historie
|
||||||
|
|
||||||
|
- **26.06.2026:** MQTT an beiden Shellys, Telegraf-Ingest, Grafana-Board. `node=garage_decke` (Kollision mit Raum-Satellit `garage`). Sensor-Zuordnung identifiziert.
|
||||||
|
- **26.06. Abend:** `reserve` → `pwt`, Fuehler auf PWT geklemmt (~26 °C → ~16 °C).
|
||||||
|
- **27.06.:** Brunnen-Fuehler isoliert; ΔT Brunnen plausibel (~2,8 K). Durchfluss **3,5 l/min** gemessen. Benchmark **684 W**.
|
||||||
|
- **27.06.:** 2. KD-Pumpe, Flussrichtung umgedreht → `kd_flussrichtung`. Grafana: ΔT KD/Brunnen, Leistung Brunnen.
|
||||||
|
- **27.06.:** Grafana-Stabilisierung (Mixed-DS, hide A/B, gruen/schwarz Hintergrund). Forgejo Issue [#132](https://git.orbitalo.net/orbitalo/homelab-brain/issues/132).
|
||||||
|
- **27.06. Abend:** ZC-T50 Verstaerkerpumpe eingebaut; **3 Pumpen in Reihe** (Test). Durchfluss **5,27 l/min** (+50 % vs. 3,5). Leistung ~772 W @ ΔT 2,1 K. Mittelfristig Pumpen reduzieren.
|
||||||
|
- **27.06.:** KD-Flussrichtung umgekehrt (Entlueftung/Optimierung). **Vorher ~20 % der Decke kalt**, danach **volle Flaeche** — VL KD von ~27 °C auf ~20 °C, ΔT ~5,5 K. Belastungstest bei ~38 °C aussen.
|
||||||
|
- **27.06. Abend — Test bestanden:** Brunnenkühlung + KaelteDecke unter Extremhitze (~38 °C aussen) hydraulisch OK. 5,27 l/min, ~772 W Brunnenleistung, volle Decke kalt. Pumpenstrom (~84 W Verstaerker + bestehend) bei PV-Ueberschuss akzeptabel. Naechster Schritt: Pumpen-Kombination vereinfachen.
|
||||||
|
|
||||||
|
## Hermes MCP (CT151, ab 28.06.2026)
|
||||||
|
|
||||||
|
Hermes fragt Brunnen/Kältedecke **nicht** per curl/ioBroker ab — festes MCP-Tool.
|
||||||
|
|
||||||
|
| Artefakt | Pfad im Repo | Deploy-Ziel (CT151) |
|
||||||
|
|----------|--------------|---------------------|
|
||||||
|
| MCP-Server | `infra/hermes/garage_decke_mcp.py` | `/root/.hermes/garage_decke_mcp.py` |
|
||||||
|
| Gateway-Config | manuell in `/root/.hermes/config.yaml` | Block `mcp_servers.garage_decke` |
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
| Tool | Funktion |
|
||||||
|
|------|----------|
|
||||||
|
| `garage_decke_live` | Alle 5 Sensoren, ΔT Brunnen/KD, Leistung @ `BRUNNEN_DURCHFLUSS`, Garage-Raum + Außen |
|
||||||
|
| `garage_decke_compare` | Jetzt vs. vor N Stunden (`hours`, default 24) |
|
||||||
|
|
||||||
|
### config.yaml (Auszug)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
garage_decke:
|
||||||
|
command: /opt/hermes-agent/.venv/bin/python3
|
||||||
|
args:
|
||||||
|
- /root/.hermes/garage_decke_mcp.py
|
||||||
|
env:
|
||||||
|
KD_FLUSSRICHTUNG: "-1"
|
||||||
|
BRUNNEN_DURCHFLUSS: "5.27"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# auf pve-mu-3:
|
||||||
|
pct push 151 garage_decke_mcp.py /root/.hermes/garage_decke_mcp.py
|
||||||
|
pct exec 151 -- chmod +x /root/.hermes/garage_decke_mcp.py
|
||||||
|
# config.yaml Block ergänzen (siehe oben)
|
||||||
|
pct exec 151 -- systemctl restart hermes-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regel (MEMORY.md):** Bei Fragen zu Brunnen/Kältedecke immer `garage_decke_live` — niemals Vorher-Werte erfinden.
|
||||||
27
infra/hermes/README.md
Normal file
27
infra/hermes/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Hermes MCP-Server (versioniert)
|
||||||
|
|
||||||
|
Python stdio MCP-Server fuer den Hermes-Gateway auf **CT151** (pve-mu-3).
|
||||||
|
|
||||||
|
Laufort: `/root/.hermes/*.py` + Eintrag in `/root/.hermes/config.yaml` unter `mcp_servers:`.
|
||||||
|
|
||||||
|
## garage_decke
|
||||||
|
|
||||||
|
Brunnen/PWT/KaelteDecke aus InfluxDB CT143 (`sensors`, `node=garage_decke`).
|
||||||
|
|
||||||
|
| Datei | Tools |
|
||||||
|
|-------|-------|
|
||||||
|
| `garage_decke_mcp.py` | `garage_decke_live`, `garage_decke_compare` |
|
||||||
|
|
||||||
|
Ausfuehrliche Doku: `docs/projekte/garage-decke-monitoring.md`
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# vom Repo auf pve-hetzner:
|
||||||
|
cd /root/homelab-brain
|
||||||
|
pct push 151 infra/hermes/garage_decke_mcp.py /root/.hermes/garage_decke_mcp.py
|
||||||
|
pct exec 151 -- chmod +x /root/.hermes/garage_decke_mcp.py
|
||||||
|
pct exec 151 -- systemctl restart hermes-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.yaml`-Block und Env-Variablen siehe Projektdoku.
|
||||||
311
infra/hermes/garage_decke_mcp.py
Executable file
311
infra/hermes/garage_decke_mcp.py
Executable file
|
|
@ -0,0 +1,311 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Garage-Decke MCP — Brunnen/PWT/Kältedecke aus InfluxDB CT143 (DB sensors).
|
||||||
|
|
||||||
|
Feste Quelle: node=garage_decke, sensor-Tags (kein ioBroker, kein Raten).
|
||||||
|
Grafana-Board: https://grafana.orbitalo.net/d/garage_decke/garage-decke
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
HOSTS = [
|
||||||
|
h.strip()
|
||||||
|
for h in os.environ.get(
|
||||||
|
"INFLUX_HOSTS",
|
||||||
|
"http://192.168.178.36:8086,http://100.66.78.56:8086",
|
||||||
|
).split(",")
|
||||||
|
if h.strip()
|
||||||
|
]
|
||||||
|
DB = "sensors"
|
||||||
|
NODE = "garage_decke"
|
||||||
|
|
||||||
|
# Aktuelle Sensor-Tags (legacy decke_1..5 / reserve ignorieren)
|
||||||
|
SENSORS = {
|
||||||
|
"rl_klimadecke": "RL Klimadecke (Rohr idx0)",
|
||||||
|
"vl_kaeltedecke": "VL Kältedecke (Rohr idx2)",
|
||||||
|
"pwt": "PWT-Oberfläche",
|
||||||
|
"vl_brunnen": "VL Brunnen (vor PWT)",
|
||||||
|
"rl_brunnen": "RL Brunnen (nach PWT)",
|
||||||
|
}
|
||||||
|
|
||||||
|
KD_FLUSSRICHTUNG = int(os.environ.get("KD_FLUSSRICHTUNG", "-1"))
|
||||||
|
BRUNNEN_LMIN = float(os.environ.get("BRUNNEN_DURCHFLUSS", "5.27"))
|
||||||
|
|
||||||
|
|
||||||
|
def _query(q: str) -> dict:
|
||||||
|
last_err = None
|
||||||
|
for host in HOSTS:
|
||||||
|
try:
|
||||||
|
url = host + "/query?" + urllib.parse.urlencode({"db": DB, "q": q})
|
||||||
|
with urllib.request.urlopen(url, timeout=8) as r:
|
||||||
|
return json.load(r)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
last_err = e
|
||||||
|
raise RuntimeError(f"InfluxDB CT143 nicht erreichbar: {last_err}")
|
||||||
|
|
||||||
|
|
||||||
|
def _last_by_sensor(within: str = "1h") -> dict[str, float]:
|
||||||
|
q = (
|
||||||
|
f'SELECT last("value") FROM "temperature" '
|
||||||
|
f'WHERE "node"=\'{NODE}\' AND time > now() - {within} GROUP BY "sensor"'
|
||||||
|
)
|
||||||
|
data = _query(q)
|
||||||
|
out: dict[str, float] = {}
|
||||||
|
for s in data.get("results", [{}])[0].get("series", []) or []:
|
||||||
|
tag = s.get("tags", {}).get("sensor")
|
||||||
|
vals = s.get("values", [])
|
||||||
|
if tag in SENSORS and vals and vals[0][1] is not None:
|
||||||
|
out[tag] = float(vals[0][1])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _node_last(node: str, within: str = "1h") -> float | None:
|
||||||
|
q = (
|
||||||
|
f'SELECT last("value") FROM "temperature" '
|
||||||
|
f'WHERE "node"=\'{node}\' AND time > now() - {within}'
|
||||||
|
)
|
||||||
|
data = _query(q)
|
||||||
|
series = data.get("results", [{}])[0].get("series")
|
||||||
|
if not series:
|
||||||
|
return None
|
||||||
|
v = series[0].get("values", [[None, None]])[0][1]
|
||||||
|
return float(v) if v is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _mean_sensor_at_hours_ago(sensor: str, hours: float) -> float | None:
|
||||||
|
center_min = int(hours * 60)
|
||||||
|
q = (
|
||||||
|
f'SELECT mean("value") FROM "temperature" '
|
||||||
|
f'WHERE "node"=\'{NODE}\' AND "sensor"=\'{sensor}\' '
|
||||||
|
f"AND time > now() - {center_min + 30}m AND time < now() - {max(center_min - 30, 0)}m"
|
||||||
|
)
|
||||||
|
data = _query(q)
|
||||||
|
series = data.get("results", [{}])[0].get("series")
|
||||||
|
if not series:
|
||||||
|
return None
|
||||||
|
v = series[0].get("values", [[None, None]])[0][1]
|
||||||
|
return float(v) if v is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _hyd_kd(rl_raw: float, vl_raw: float, direction: int = KD_FLUSSRICHTUNG):
|
||||||
|
a, b = rl_raw, vl_raw
|
||||||
|
rl_h = (a + b - direction * (b - a)) / 2
|
||||||
|
vl_h = (a + b + direction * (b - a)) / 2
|
||||||
|
return vl_h, rl_h, rl_h - vl_h
|
||||||
|
|
||||||
|
|
||||||
|
def _calc(vals: dict[str, float]) -> dict:
|
||||||
|
vb = vals.get("vl_brunnen")
|
||||||
|
rb = vals.get("rl_brunnen")
|
||||||
|
dt_b = (rb - vb) if vb is not None and rb is not None else None
|
||||||
|
power_w = BRUNNEN_LMIN * dt_b * 69.8 if dt_b is not None else None
|
||||||
|
|
||||||
|
kd_vl = kd_rl = dt_kd = None
|
||||||
|
if "rl_klimadecke" in vals and "vl_kaeltedecke" in vals:
|
||||||
|
kd_vl, kd_rl, dt_kd = _hyd_kd(vals["rl_klimadecke"], vals["vl_kaeltedecke"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"dt_brunnen": dt_b,
|
||||||
|
"power_brunnen_w": power_w,
|
||||||
|
"kd_vl_hyd": kd_vl,
|
||||||
|
"kd_rl_hyd": kd_rl,
|
||||||
|
"dt_kd": dt_kd,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_live(_args: dict) -> str:
|
||||||
|
vals = _last_by_sensor()
|
||||||
|
missing = [k for k in SENSORS if k not in vals]
|
||||||
|
if len(vals) < 3:
|
||||||
|
return (
|
||||||
|
f"Zu wenig Daten (node={NODE}, DB={DB}). "
|
||||||
|
f"Fehlend: {', '.join(missing) or 'unbekannt'}. "
|
||||||
|
"Nicht ioBroker — nur Influx CT143 sensors."
|
||||||
|
)
|
||||||
|
|
||||||
|
calc = _calc(vals)
|
||||||
|
garage = _node_last("garage")
|
||||||
|
aussen = _node_last("aussen") or _node_last("oelraum")
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"Garage Decke — Live (InfluxDB sensors, node=garage_decke)",
|
||||||
|
f"KD Flussrichtung: {KD_FLUSSRICHTUNG} | Brunnen angenommen: {BRUNNEN_LMIN} l/min",
|
||||||
|
"",
|
||||||
|
"Brunnenkreis (PWT):",
|
||||||
|
f"- VL Brunnen: {vals.get('vl_brunnen', 0):.1f} °C",
|
||||||
|
f"- RL Brunnen: {vals.get('rl_brunnen', 0):.1f} °C",
|
||||||
|
f"- ΔT Brunnen: {calc['dt_brunnen']:.2f} K" if calc["dt_brunnen"] is not None else "- ΔT Brunnen: n/a",
|
||||||
|
f"- PWT-Oberfläche: {vals.get('pwt', 0):.1f} °C",
|
||||||
|
]
|
||||||
|
if calc["power_brunnen_w"] is not None:
|
||||||
|
lines.append(f"- Leistung Brunnen: {calc['power_brunnen_w']:.0f} W")
|
||||||
|
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"Kältedecke (hydraulisch, per kd_flussrichtung):",
|
||||||
|
f"- VL KD: {calc['kd_vl_hyd']:.1f} °C" if calc["kd_vl_hyd"] is not None else "- VL KD: n/a",
|
||||||
|
f"- RL KD: {calc['kd_rl_hyd']:.1f} °C" if calc["kd_rl_hyd"] is not None else "- RL KD: n/a",
|
||||||
|
f"- ΔT KD: {calc['dt_kd']:.2f} K" if calc["dt_kd"] is not None else "- ΔT KD: n/a",
|
||||||
|
"",
|
||||||
|
"Rohr-Fühler (Influx-Raw):",
|
||||||
|
f"- rl_klimadecke: {vals.get('rl_klimadecke', 0):.1f} °C",
|
||||||
|
f"- vl_kaeltedecke: {vals.get('vl_kaeltedecke', 0):.1f} °C",
|
||||||
|
]
|
||||||
|
|
||||||
|
ctx = []
|
||||||
|
if garage is not None:
|
||||||
|
ctx.append(f"Garage-Raum: {garage:.1f} °C")
|
||||||
|
if aussen is not None:
|
||||||
|
ctx.append(f"Außen: {aussen:.1f} °C")
|
||||||
|
if ctx:
|
||||||
|
lines += ["", "Kontext:", *[f"- {c}" for c in ctx]]
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
lines += ["", f"Hinweis fehlende Tags: {', '.join(missing)}"]
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def tool_compare(args: dict) -> str:
|
||||||
|
hours = float(args.get("hours", 24))
|
||||||
|
now = _last_by_sensor(within="30m")
|
||||||
|
if not now.get("vl_brunnen") or not now.get("rl_brunnen"):
|
||||||
|
return "Aktuelle Brunnen-Daten fehlen — garage_decke_live zuerst prüfen."
|
||||||
|
|
||||||
|
past_vals: dict[str, float] = {}
|
||||||
|
for sensor in SENSORS:
|
||||||
|
m = _mean_sensor_at_hours_ago(sensor, hours)
|
||||||
|
if m is not None:
|
||||||
|
past_vals[sensor] = m
|
||||||
|
|
||||||
|
if len(past_vals) < 3:
|
||||||
|
return f"Keine Vergleichsdaten vor {hours:.0f} h (Influx leer?)."
|
||||||
|
|
||||||
|
c_now = _calc(now)
|
||||||
|
c_past = _calc(past_vals)
|
||||||
|
|
||||||
|
def _delta(label, a, b, unit="K"):
|
||||||
|
if a is None or b is None:
|
||||||
|
return f"- {label}: n/a"
|
||||||
|
d = a - b
|
||||||
|
return f"- {label}: jetzt {a:.2f}{unit}, vor {hours:.0f}h {b:.2f}{unit} (Δ {d:+.2f}{unit})"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"Garage Decke — Vergleich jetzt vs. vor ~{hours:.0f} h",
|
||||||
|
"",
|
||||||
|
"Brunnen:",
|
||||||
|
_delta("ΔT Brunnen", c_now["dt_brunnen"], c_past["dt_brunnen"]),
|
||||||
|
]
|
||||||
|
if c_now["power_brunnen_w"] is not None and c_past["power_brunnen_w"] is not None:
|
||||||
|
lines.append(
|
||||||
|
_delta("Leistung Brunnen", c_now["power_brunnen_w"], c_past["power_brunnen_w"], " W").replace("K", "W")
|
||||||
|
)
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
"Kältedecke:",
|
||||||
|
_delta("ΔT KD", c_now["dt_kd"], c_past["dt_kd"]),
|
||||||
|
"",
|
||||||
|
"Einzelwerte jetzt:",
|
||||||
|
f"- vl_brunnen {now.get('vl_brunnen', 0):.1f} °C, rl_brunnen {now.get('rl_brunnen', 0):.1f} °C",
|
||||||
|
f"- pwt {now.get('pwt', 0):.1f} °C",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = [
|
||||||
|
{
|
||||||
|
"name": "garage_decke_live",
|
||||||
|
"description": (
|
||||||
|
"Aktuelle Garage-Decke/Brunnen-Sensoren aus InfluxDB (node=garage_decke): "
|
||||||
|
"vl/rl_brunnen, PWT, Kältedecke VL/RL, ΔT, Leistung Brunnen. "
|
||||||
|
"IMMER dieses Tool nutzen — nicht curl/raten."
|
||||||
|
),
|
||||||
|
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "garage_decke_compare",
|
||||||
|
"description": (
|
||||||
|
"Vergleicht Garage-Decke-Werte jetzt vs. vor N Stunden (default 24). "
|
||||||
|
"Keine erfundenen Vorher-Werte."
|
||||||
|
),
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"hours": {"type": "number", "description": "Stunden zurück (default 24)"}},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
HANDLERS = {
|
||||||
|
"garage_decke_live": tool_live,
|
||||||
|
"garage_decke_compare": tool_compare,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle(req):
|
||||||
|
method = req.get("method")
|
||||||
|
rid = req.get("id")
|
||||||
|
if method == "initialize":
|
||||||
|
return {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": rid,
|
||||||
|
"result": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {"tools": {}},
|
||||||
|
"serverInfo": {"name": "garage_decke", "version": "1.0.0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if method in ("notifications/initialized", "initialized"):
|
||||||
|
return None
|
||||||
|
if method == "tools/list":
|
||||||
|
return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
|
||||||
|
if method == "tools/call":
|
||||||
|
name = req.get("params", {}).get("name")
|
||||||
|
args = req.get("params", {}).get("arguments", {}) or {}
|
||||||
|
handler = HANDLERS.get(name)
|
||||||
|
try:
|
||||||
|
text = handler(args) if handler else f"Unbekanntes Tool: {name}"
|
||||||
|
except Exception as e:
|
||||||
|
text = f"Fehler: {e}"
|
||||||
|
return {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": rid,
|
||||||
|
"result": {"content": [{"type": "text", "text": text}]},
|
||||||
|
}
|
||||||
|
if method == "ping":
|
||||||
|
return {"jsonrpc": "2.0", "id": rid, "result": {}}
|
||||||
|
return {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": rid,
|
||||||
|
"error": {"code": -32601, "message": f"Unknown: {method}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
req = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
resp = handle(req)
|
||||||
|
except Exception as e:
|
||||||
|
resp = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": req.get("id"),
|
||||||
|
"error": {"code": -32603, "message": str(e)},
|
||||||
|
}
|
||||||
|
if resp is not None:
|
||||||
|
sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -99,7 +99,7 @@ Panels (ab 27.06.2026):
|
||||||
- **ΔT KaelteDecke** = RL − VL KaelteDecke (Effektivitaet, kein Durchfluss)
|
- **ΔT KaelteDecke** = RL − VL KaelteDecke (Effektivitaet, kein Durchfluss)
|
||||||
- **ΔT Brunnen** = RL − VL Brunnen; **Leistung** [kW] = `brunnen_durchfluss` [l/min, 3–4,5] × ΔT × 0,0698
|
- **ΔT Brunnen** = RL − VL Brunnen; **Leistung** [kW] = `brunnen_durchfluss` [l/min, 3–4,5] × ΔT × 0,0698
|
||||||
|
|
||||||
Ausführliche Doku: CT999 `/root/docs/projekte/garage-decke-monitoring.md`
|
Ausführliche Doku: `docs/projekte/garage-decke-monitoring.md` (Spiegel: CT999 `/root/docs/projekte/`)
|
||||||
|
|
||||||
Shelly Gen1: MQTT aktiv → Shelly-App zeigt offline (Cloud vs. MQTT).
|
Shelly Gen1: MQTT aktiv → Shelly-App zeigt offline (Cloud vs. MQTT).
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue