- Python 78.9%
- Shell 9.9%
- JavaScript 8.4%
- CSS 2%
- HTML 0.6%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| app | ||
| deploy | ||
| grafana | ||
| .env.example | ||
| .gitignore | ||
| DEPLOYMENT.md | ||
| docker-compose.yml | ||
| README.md | ||
| TODO.md | ||
IoT Monitor
Sammelt Klima-, Strom- und Solardaten in InfluxDB und zeigt sie in Grafana oder einer schlanken eigenen Web-UI. Vier Quellen: Netatmo, Tapo, Tibber, Deye.
| Quelle | Measurement | Werte |
|---|---|---|
| Netatmo Wetterstation | climate |
temperature, humidity, co2, noise, pressure, rain, wind_*, battery, rf_strength |
| Tapo P110/P115 | power |
power_w, energy_today_wh, energy_month_wh, voltage_v, current_a, is_on, rssi |
| Tibber Pulse | tibber_live |
power_w, power_production_w, voltage_l1..3, current_l1..3, accumulated_* |
| Tibber Preise | tibber_price |
total, energy, tax, level — Stundenpreise fuer heute und morgen |
| Tibber Verbrauch | tibber_consumption |
consumption_kwh, cost, unit_price — stuendlich, mit Verzug |
| Deye Wechselrichter | inverter |
pv_power_w, battery_soc_pct, grid_import_w, grid_export_w, load_power_w, daily_*_kwh — lokal via Modbus oder via Deye Cloud |
Tags: device (Modul-, Steckdosen- bzw. Anlagenname), home (Netatmo-Zuhause).
Die Preise tragen zukuenftige Zeitstempel — die Day-Ahead-Kurve laesst sich so neben den Verbrauch legen.
Start
cp .env.example .env
# INFLUX_TOKEN, Admin-Passwoerter und Geraete-Credentials eintragen
docker compose up -d
Netatmo einmalig autorisieren (siehe unten), dann den Collector als Dienst starten:
docker compose --profile collector up -d collector
| Dienst | Adresse | Zweck |
|---|---|---|
| Web-UI | http://localhost:8081 | schlanke Diagrammansicht, aktualisiert sich selbst |
| Grafana | http://localhost:3000 | vollstaendiges Dashboard inkl. Alerts |
| InfluxDB | http://localhost:8086 | Datenbank |
Alle Ports sind auf 127.0.0.1 gebunden, weil der Stack mit schwachen
Standardpasswoertern ausgeliefert wird. Fuer LAN-Zugriff erst Passwoerter
aendern, dann BIND_ADDR=0.0.0.0 in die .env setzen — das oeffnet Grafana
und die Web-UI. InfluxDB bleibt auf 127.0.0.1.
Docker ist die Entwicklungsumgebung. Auf dem internen Server laeuft der Stack ohne Docker — InfluxDB, Grafana und die beiden Python-Dienste als native systemd-Units. Einrichtung und Betrieb: DEPLOYMENT.md.
./deploy/deploy.sh
Dienste
| Service | Start | Beschreibung |
|---|---|---|
influxdb, grafana, web |
docker compose up -d |
laufen dauerhaft |
collector |
docker compose --profile collector up -d collector |
eigener Dienst mit restart: unless-stopped |
app |
docker compose up -d app |
Dev-Container: Shell, Tests, Einmal-Skripte |
Logs und Tests:
docker compose logs -f collector
docker compose exec app python -m pytest
docker compose exec app ruff check .
Netatmo einrichten
Netatmo hat den Password-Grant abgeschaltet — noetig ist der OAuth2-Authorization-Code-Flow, einmal pro Installation.
-
App auf https://dev.netatmo.com/apps/ anlegen.
NETATMO_CLIENT_IDundNETATMO_CLIENT_SECRETin die.enveintragen. -
Dort dieselbe Redirect-URI hinterlegen, die in
NETATMO_REDIRECT_URIsteht (Defaulthttp://localhost:8080/callback). Sie muss zeichengenau uebereinstimmen. -
Autorisierung durchfuehren:
docker compose exec app python netatmo_login.pyDas Skript gibt eine URL aus. Im Browser oeffnen, Zugriff bestaetigen — die Weiterleitung laeuft danach ins Leere, das ist so gewollt. Die komplette Adresse aus der Adresszeile zurueck ins Skript kopieren.
Der Refresh-Token landet in data/secrets/netatmo_token.json und uebersteht
einen Rebuild. Netatmo rotiert ihn bei jeder Erneuerung, der Collector schreibt
die neue Version sofort zurueck. Geht die Datei verloren, einfach
netatmo_login.py erneut ausfuehren.
Angefragte Scopes: read_station, read_homecoach.
Geraetenamen
Der Tag device kommt aus dem Namen, den das Modul in der Netatmo-App traegt.
Unbenannte Module bekommen ein Label nach Geraetetyp (Aussenmodul,
Regenmesser, …), die Basisstation den Namen des Zuhauses. Wer das aendern
will, benennt das Modul in der Netatmo-App um — der Collector zieht beim
naechsten Lauf nach. Achtung: das erzeugt eine neue Zeitreihe, die alte bleibt
unter dem alten Namen liegen.
Tapo einrichten
Die Steckdosen werden lokal im LAN abgefragt. TAPO_USER / TAPO_PASSWORD
sind die Zugangsdaten des Tapo-Cloud-Accounts — aktuelle Firmware braucht sie
fuer den lokalen Handshake, der Traffic laeuft nicht ueber TP-Link.
TAPO_HOSTS mit den IP-Adressen der Steckdosen fuellen, kommagetrennt:
TAPO_HOSTS=192.168.2.150,192.168.2.38
Im Router per DHCP-Reservierung festnageln. Ist TAPO_HOSTS leer, versucht der
Collector eine Broadcast-Discovery — die funktioniert im Docker-Bridge-Netz
nicht.
Wenn die Steckdose „Unsupported device … TPAP" meldet
Neuere Tapo-Firmware kuendigt encrypt_type: TPAP an (PAKE-basiert). Keine
veroeffentlichte python-kasa-Version spricht das; upstream laeuft das unter
Issue #1590.
Workaround: in der Tapo-App unter Geraeteeinstellungen die Option Kompatibilitaet mit Drittanbietern aus- und wieder einschalten. Die Steckdose faellt dann auf KLAP zurueck, das unterstuetzt wird.
Tibber einrichten
Persoenlichen Zugriffstoken auf https://developer.tibber.com/ erzeugen
(Self-Service, kostenlos) und als TIBBER_TOKEN in die .env eintragen. Mehr
ist nicht noetig — der Collector fragt beim Start ab, welche Zuhause am Token
haengen und ob dort ein Pulse gemeldet ist, und schaltet die Live-Messung
entsprechend zu oder ab.
Preise und stuendlicher Verbrauch funktionieren mit jedem Tarif. Mit TIBBER_CONTINUOUS=true (Standard) hält ein Hintergrund-Worker die
Pulse-Verbindung offen und speichert jede empfangene Live-Messung. Bei
Verbindungsfehlern verbindet er sich erneut. Preise und Verbrauch laufen weiter
im normalen Scheduler. TIBBER_CONTINUOUS=false stellt die bisherige
Stichprobenmessung pro Collector-Lauf wieder her.
Deye einrichten
Ausgelesen wird lokal ueber den Solarman-Logger-Stick, ohne Cloud:
DEYE_HOST=192.168.2.60 # IP des Sticks
DEYE_SERIAL=1234567890 # 10-stellige Nummer auf dem Aufkleber
Wichtig: die Registerkarte in collectors/deye_registers.py stammt aus dem
home_assistant_solarman-Projekt fuer deye_sg04lp3. Deye aendert die
Adressen je nach Modellfamilie und Firmware. Vor dem ersten Vertrauen also:
docker compose exec app python deye_scan.py
Das Skript liest alle Register des Profils und zeigt Rohwert und dekodierten
Wert nebeneinander. Gegen das Display des Wechselrichters oder die Solarman-App
vergleichen. Weicht etwas ab, mit --raw die Umgebung absuchen:
docker compose exec app python deye_scan.py --raw 0x0240 0x0260
Die Ausgabe zeigt jede plausible Dekodierung nebeneinander (u16, s16, x0.1, x0.01), sodass sich die richtige Adresse meist auf einen Blick findet.
Zu pruefen ist ausserdem das Vorzeichen: der Collector nimmt an, dass
grid_power_w positiv Bezug bedeutet und battery_power_w positiv Ladung.
Daraus leitet er grid_import_w/grid_export_w und
battery_charge_w/battery_discharge_w ab. Stimmt die Annahme nicht, drehen
sich diese vier Werte um.
Was steckt in der Datenbank?
docker compose exec app python influx_stats.py
/opt/iot-monitor/deploy/run.sh influx_stats.py --days 7
Zeigt je Measurement Serien, Felder, Punkte und den abgedeckten Zeitraum,
danach die Punkte je Geraet. Liest mit INFLUX_READ_TOKEN — denselben
Zugangsdaten wie die UI — und beantwortet damit auch die Frage, ob dieser
Token ueberhaupt etwas sieht.
Zeigt Grafana „no data", die Web-UI aber Werte, ist fast immer der Bucketname
schuld: das Dashboard holt ihn ueber die Variable bucket, die alle nicht
systeminternen Buckets auflistet. Steht dort der falsche, oben im Dashboard
umschalten.
Verhalten bei Fehlern
Zwei Regeln, die fuer alle Collector gelten und in
tests/test_partial_failures.py festgehalten sind:
1. Ein Ausfall bricht nicht den ganzen Lauf ab. Eine tote Steckdose kostet nicht die anderen; ein Pulse, der wegbricht, kostet nicht die Preiskurve; eine Quelle, die komplett scheitert, kostet nicht die uebrigen drei.
2. Unvollstaendiges wird nicht gespeichert. Ein halb gelesener Messwert sieht in Grafana aus wie ein gesundes Geraet, das nichts verbraucht — und das ist schlimmer als eine Luecke in der Kurve. Konkret:
| Fall | Verhalten |
|---|---|
| Steckdose mit Energiezaehler liefert keine Zaehlerwerte | ganzer Messwert verworfen (nicht nur is_on schreiben) |
| Steckdose ohne Energiezaehler (z. B. Lampe) | is_on/rssi werden gespeichert — das ist kein Fehler |
| Deye: ein Registerblock nicht lesbar | ganzer Snapshot verworfen |
| Deye: ein Register dekodiert nicht | ganzer Snapshot verworfen |
| Tibber: eine der drei Abfragen scheitert | die beiden anderen werden gespeichert |
| Netatmo-Modul nicht erreichbar | Modul uebersprungen, andere Module bleiben |
Der Deye ist bewusst streng: ein Snapshot aus Bloecken, die zu verschiedenen Zeitpunkten gelesen wurden, waere in sich widerspruechlich — PV aus dieser Sekunde, Batterie von irgendwann.
Teilausfaelle sind im Log als WARNING sichtbar, und die Zusammenfassung
zaehlt gegen die konfigurierten Geraete (Tapo: 1/3 device(s) read), nicht
gegen die, die geantwortet haben.
Deye Cloud statt lokal
Neuere Deye-Dongles bieten den Solarman-Port 8899 nicht mehr an — dann bleibt
der offizielle Cloud-Weg. Konto auf https://developer.deyecloud.com/ anlegen,
dort eine App erstellen, DEYE_CLOUD_APP_ID und DEYE_CLOUD_APP_SECRET
uebernehmen, dazu E-Mail und Passwort des Deye-Cloud-Kontos. Das Passwort geht
als SHA-256-Hash an die API, nicht im Klartext — das verlangt die API so.
docker compose exec app python deye_cloud_probe.py
Das Skript authentifiziert sich, listet die Stationen und zeigt fuer jede, was
station/latest liefert: welche Felder uebernommen werden und welche es
zusaetzlich gaebe. Passt die Zuordnung nicht, ist sie in
collectors/deye_cloud.py unter STATION_FIELDS in einer Zeile korrigiert.
Nur eine der beiden Varianten konfigurieren — sind DEYE_HOST und
DEYE_CLOUD_APP_ID gleichzeitig gesetzt, schreiben beide dasselbe Measurement
und der Collector warnt beim Start.
Preis dieses Wegs: die Werte sind einige Minuten alt, es gibt Ratenlimits, und die Daten laufen ueber Deyes Server.
Konfiguration
Alle Werte kommen aus .env (siehe .env.example).
| Variable | Default | Bedeutung |
|---|---|---|
INFLUX_TOKEN |
— | Admin-Token, nur noch fuer Verwaltung |
INFLUX_WRITE_TOKEN |
INFLUX_TOKEN |
Schreibrecht auf sensors, fuer den Collector |
INFLUX_READ_TOKEN |
INFLUX_TOKEN |
Leserecht, fuer Grafana und die Web-UI |
INFLUX_ADMIN_PASSWORD |
— | InfluxDB-Admin, nur beim ersten Start wirksam |
GRAFANA_ADMIN_PASSWORD |
— | Grafana-Admin, nur beim ersten Start wirksam |
COLLECT_INTERVAL |
300 |
Abstand zwischen zwei Laeufen in Sekunden |
NETATMO_CLIENT_ID / _SECRET |
— | Credentials der Netatmo-App |
NETATMO_REDIRECT_URI |
http://localhost:8080/callback |
muss bei Netatmo registriert sein |
NETATMO_TOKEN_PATH |
/secrets/netatmo_token.json |
Ablage des Refresh-Tokens |
TAPO_USER / TAPO_PASSWORD |
— | Tapo-Cloud-Account |
TAPO_HOSTS |
— | Steckdosen-IPs, kommagetrennt |
TAPO_TIMEOUT |
10 |
Timeout pro Steckdose in Sekunden |
TIBBER_TOKEN |
— | Zugriffstoken von developer.tibber.com |
TIBBER_CONSUMPTION_HOURS |
24 |
Wie viele Verbrauchsstunden je Lauf geholt werden |
DEYE_HOST |
— | IP des Solarman-Logger-Sticks |
DEYE_SERIAL |
— | Seriennummer des Sticks |
DEYE_PORT |
8899 |
Port des Sticks |
DEYE_NAME |
Deye |
Geraetename im device-Tag |
DEYE_PROFILE |
sg04lp3 |
Registerkarte, siehe collectors/deye_registers.py |
DEYE_CLOUD_APP_ID / _APP_SECRET |
— | App-Credentials von developer.deyecloud.com |
DEYE_CLOUD_EMAIL / _PASSWORD |
— | Konto der Deye Cloud |
DEYE_CLOUD_REGION |
eu1 |
Rechenzentrum (eu1 oder us1) |
DEYE_CLOUD_STATION_ID |
— | optional, sonst Autoerkennung |
LOG_DIAGNOSE |
false |
Variablenwerte an Tracebacks haengen (nur zum Debuggen) |
WEB_PORT |
8081 |
Port der Web-UI |
WEB_REFRESH |
30 |
Aktualisierungsintervall der Web-UI in Sekunden |
BIND_ADDR |
127.0.0.1 |
Bind-Adresse von Grafana und Web-UI, 0.0.0.0 fuer LAN |
LOG_PATH |
app.log |
Pfad der rotierenden Logdatei |
LOG_LEVEL |
INFO |
Loguru-Level |
Web-UI
Abhaengigkeitsfrei: die Diagramme sind handgezeichnetes SVG, kein CDN, kein
Build-Schritt. Zeitraum ueber die Schalter oben rechts (1h bis 30d), die
Auswahl landet in der URL. Aktualisiert sich alle WEB_REFRESH Sekunden und
pausiert, solange der Tab im Hintergrund liegt.
Mit dem Mauszeiger ueber ein Diagramm faehrt man eine Fadenkreuz-Anzeige: sie zeigt Zeitpunkt und die Werte aller Serien an dieser Stelle. Solange der Zeiger im Diagramm steht, wird nicht neu geladen — sonst waere der Tooltip mitten im Lesen weg.
Die Diagramme werden in der tatsaechlichen Pixelbreite ihrer Karte gezeichnet,
nicht in eine feste viewBox skaliert. Vorher schrumpfte die Beschriftung
zusammen mit dem Rest auf etwa 5 px.
Grafana kann das alles besser — die Web-UI ist die leichtgewichtige, loginfreie Variante fuer ein Wandtablet oder den schnellen Blick.
Aufbau
app/
main.py Scheduler, Einstiegspunkt des Collectors
web.py aiohttp-Server der Web-UI
webquery.py Flux-Abfragen hinter der Web-UI
netatmo_login.py Einmalige OAuth2-Autorisierung
deye_scan.py Register gegen das echte Geraet pruefen
deye_cloud_probe.py Deye-Cloud-Antwort inspizieren
config.py Konfiguration aus der Umgebung
collectors/
collector.py Fasst die Quellen zusammen
netatmo.py Wetterstation via pyatmo 9 (AsyncAccount)
netatmo_auth.py OAuth2-Token-Handling und -Ablage
tapo.py P110/P115 via python-kasa
tibber.py Preise, Verbrauch und Pulse-Livewerte
deye.py Wechselrichter via Solarman-Logger (lokal)
deye_registers.py Modbus-Registerkarten
deye_cloud.py Wechselrichter via Deye Cloud (OpenAPI)
storage/influx.py InfluxDB-Writer
static/ HTML, CSS, JS der Web-UI
tests/
grafana/provisioning/
datasources/ InfluxDB-Anbindung
dashboards/ Dashboard „IoT Monitor"
alerting/ Alerts: keine Daten, CO2 dauerhaft hoch
deploy/
server-setup.sh Einmalige Servereinrichtung, laeuft dort als root
deploy.sh Deployment auf den internen Server
run.sh Einmal-Skripte auf dem Server, ohne Docker
data/influxdb/ Persistente InfluxDB-Daten
data/secrets/ Netatmo-Refresh-Token
Offene Punkte: TODO.md.
Erweiterte Auswertungen und Aktualisierung
Die Web-UI enthält 60 Diagramme in den Bereichen Klima/Wetter, Geräte, Strom/Tarif und Solar/Speicher. Grafana enthält ebenfalls die zusätzlichen Rohwerte. Nicht unterstützte Messwerte bleiben ausdrücklich ohne Daten.
- Klima: Taupunkt und absolute Feuchte werden aus zeitgleichen Temperatur-
und Feuchtemessungen berechnet. Das Außenmodul wird im Auswahlfeld zugeordnet;
die Auswahl bleibt lokal im Browser gespeichert. Optional setzt
NETATMO_OUTDOOR_DEVICEden Standard-Gerätenamen. CO₂-Hinweise verwenden 1000 ppm als Orientierungsschwelle und zeigen die Änderung der letzten 30 Minuten. - Geräte: Schätzungen für Betriebszustand und Laufzeit nutzen eine 5-W-Schwelle. Ein Abfall darunter kann als „möglicherweise fertig“ erscheinen. Dies ist keine gerätespezifische Fertigerkennung. Kosten entstehen aus Zählerdifferenzen und zeitlich passenden Tibber-Preisen, mit gleichmäßiger Verteilung innerhalb des Messintervalls. Negative Preise werden unterstützt. Es muss genau ein Tarif-Zuhause vorliegen; die Zuordnung wird angezeigt. Zählerrücksetzungen und Messlücken werden ausgelassen, die abgedeckte Zeit wird angegeben. Grundgebühr und eine mögliche Versorgung aus eigener PV sind in dieser Bewertung zum Netzstromtarif nicht berücksichtigt.
- Solar: Tages-Autarkie =
100 × (1 − Netzbezug / Hausverbrauch); Tages-Eigenverbrauch =100 × (1 − Einspeisung / PV-Erzeugung). Nur zeitgleiche, plausible Tageszähler werden verwendet. Speicheränderungen, Netzladung und Batterieeinspeisung können diese Bilanzschätzungen verzerren. Der Cloud- Collector liefert derzeit keine passenden Tageszähler; dort bleiben diese beiden Diagramme leer. Die verfügbaren Leistungsflüsse werden angezeigt. - Sensorstatus: letzte Original-Messzeit aus 30 Tagen, veraltete Daten, Batterie bis 20 %, Tapo-WLAN unter −75 dBm sowie Überlast/Übertemperatur. Als veraltet gilt ein Alter über dem größeren Wert aus 15 Minuten und drei Collector-Intervallen. Geräte ohne Messung in den letzten 30 Tagen sind nicht enthalten. Funk-Rohwerte werden ohne geratenen geräteübergreifenden Grenzwert angezeigt.
„Jetzt aktualisieren“ lädt nur Daten nach. Bestehende Karten bleiben erhalten, und die sichtbare Stelle wird bei manueller und automatischer Aktualisierung wiederhergestellt. Ein normaler Browser-Reload merkt sich ebenfalls die Scrollposition innerhalb des Tabs. Fehlgeschlagene Abfragen lassen vorhandene Diagramme stehen. Verspätete Antworten können neuere Daten nicht überschreiben.
Zur Begrenzung der Datenmenge liest die UI kontinuierliche Pulse-Daten als Minimum, Maximum und letzten Messwert je Zeitfenster. Für Diagramme werden zusätzlich Extremwerte und Endpunkte erhalten, während die Punktzahl reduziert wird. Die Kostenberechnung nutzt die unverdichteten Steckdosen-Zählerwerte.
Prüfung: docker compose exec -T web python -m pytest und
docker compose exec -T web ruff check .. Die DOM-Regressionstests benötigen
jsdom als reine Testabhängigkeit, beispielsweise:
npm install --prefix /tmp/iot-ui-check jsdom --no-audit --no-fund
NODE_PATH=/tmp/iot-ui-check/node_modules node --test app/tests/ui_refresh.cjs
Nach Aktualisierung des Codes Web- und Collector-Dienst neu starten. Wenn eine Compose-Umgebungsvariable geändert wurde, die betroffenen Container neu erstellen.
Ansichten per YAML ein- und ausblenden
Die Sichtbarkeit der Web-UI steuerst du in app/views.yaml.
Die Datei enthält alle Diagramm- und Auswertungsschlüssel mit Kommentaren.
true zeigt eine Ansicht, false blendet sie aus. Zum Beispiel:
default_visible: true
groups:
solar: false # Bereich Solar & Speicher samt zugehörigen Hinweisen
insights: true # Auswertungskarten insgesamt anzeigen
panels:
noise: false # Geräuschpegel ausblenden
sensor_battery: false
temperature: true
insights:
device_state: false
device_costs: true
groups: Bereichsschalterclimate,power,grid,solarundinsights. Fehlende Bereichsschalter sind eingeschaltet. Ein ausgeschalteter Bereich hat Vorrang vor seinen Einzelansichten.panels: einzelne Diagramme, einschließlich berechneter Diagramme wiedew_point,runtime,autonomyundself_use.insights: einzelne Arten von Auswertungskarten.sensor_statusundcoveragesind bereichsübergreifend; sie lassen sich separat oder mitgroups.insights: falseabschalten.default_visible: Standard für nicht aufgeführte Diagramme und Hinweise. Für eine Positivliste setze ihn in einer gekürzten Konfiguration auffalseund führe nur die gewünschten Einträge mittrueauf.
Die YAML-Datei wird bei jedem Datenabruf erneut gelesen. Nach dem Speichern reicht „Jetzt aktualisieren“, alternativ greift die automatische Aktualisierung. Ausgeblendete Karten, leere Bereichsüberschriften und ihre Navigationslinks verschwinden. Beim Wiedereinblenden gilt die gewohnte Reihenfolge. Die Außenmodul-Auswahl bleibt auch dann verwendbar, wenn das Temperaturdiagramm ausgeblendet ist.
Sammlung, Speicherung und Berechnung bleiben unabhängig von der Sichtbarkeit. Die Collector lesen diese Datei nicht. Ausgeblendete Rohwerte stehen weiterhin für berechnete Ansichten zur Verfügung. Grafana hat eine eigene Darstellung und wird von dieser Web-UI-Konfiguration nicht gefiltert.
Ungültiges YAML, unbekannte Schlüssel, doppelte Einträge und Werte wie
"false" statt false werden als Konfigurationsfehler angezeigt. Bestehende
Diagramme bleiben bei einem solchen Fehler stehen. Nach Korrektur genügt ein
weiterer Daten-Reload; ein Dienstneustart ist nicht nötig.
Die mitgelieferte Datei aktiviert zunächst alle Ansichten. In Docker liegt sie
im bestehenden ./app-Bind-Mount. Beim regulären Deployment wird die lokale
Datei nach /opt/iot-monitor/app/views.yaml übertragen und vor dem Neustart
validiert. Änderungen direkt auf dem Server werden beim nächsten Deployment
mit der lokalen Datei überschrieben. Die zusätzliche Abhängigkeit PyYAML
wird über app/requirements.txt installiert; bestehende Docker-Images beim
nächsten Neuaufbau mit docker compose build web collector aktualisieren.