No description
  • Python 78.9%
  • Shell 9.9%
  • JavaScript 8.4%
  • CSS 2%
  • HTML 0.6%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-08 11:51:32 +02:00
app Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
deploy Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
grafana Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
.env.example Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
.gitignore Initial commit: IoT monitor for Netatmo and Tapo 2026-08-05 20:33:18 +02:00
DEPLOYMENT.md Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
docker-compose.yml Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
README.md Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00
TODO.md Add sensor insights, continuous Pulse recording and configurable dashboard views 2026-09-08 11:51:32 +02:00

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.

  1. App auf https://dev.netatmo.com/apps/ anlegen. NETATMO_CLIENT_ID und NETATMO_CLIENT_SECRET in die .env eintragen.

  2. Dort dieselbe Redirect-URI hinterlegen, die in NETATMO_REDIRECT_URI steht (Default http://localhost:8080/callback). Sie muss zeichengenau uebereinstimmen.

  3. Autorisierung durchfuehren:

    docker compose exec app python netatmo_login.py
    

    Das 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_DEVICE den 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: Bereichsschalter climate, power, grid, solar und insights. Fehlende Bereichsschalter sind eingeschaltet. Ein ausgeschalteter Bereich hat Vorrang vor seinen Einzelansichten.
  • panels: einzelne Diagramme, einschließlich berechneter Diagramme wie dew_point, runtime, autonomy und self_use.
  • insights: einzelne Arten von Auswertungskarten. sensor_status und coverage sind bereichsübergreifend; sie lassen sich separat oder mit groups.insights: false abschalten.
  • default_visible: Standard für nicht aufgeführte Diagramme und Hinweise. Für eine Positivliste setze ihn in einer gekürzten Konfiguration auf false und führe nur die gewünschten Einträge mit true auf.

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.