Skip to content

Technik Blog

Programmieren | Arduino | ESP32 | MicroPython | Python | Raspberry Pi | Raspberry Pi Pico

Menu
  • Smarthome
  • Gartenautomation
  • Arduino
  • ESP32 & Co.
  • Raspberry Pi & Pico
  • Solo Mining
  • Deutsch
  • English
Menu

Luftqualität live: air-Q pro Daten via REST abrufen und anzeigen

Posted on 14. August 202514. August 2025 by Stefan Draeger

Wer meinen air-Q pro im ausführlichen Review schon kennt, weiß: Das Gerät misst beeindruckend viele Werte – von CO₂ und TVOC über Temperatur und Luftfeuchte bis hin zu Feinstaub, Lärm und Luftdruck. In diesem Beitrag gehen wir den nächsten Schritt: Wir bringen diese Messdaten live auf ein LCD1602-Display – direkt dort, wo du sie brauchst, ohne App und ohne Cloud.

Als Anzeige nutze ich mein universelles MakerDisplay. Das Prinzip ist simpel: Der air-Q liefert die Daten, ein kleines Python-Skript holt sie per REST ab, entschlüsselt sie und schickt sie per HTTP ans Display. So rotieren die 13 Messgrößen plus die beiden virtuellen Werte „Gesundheit“ und „Leistung“ nacheinander über zwei Zeilen – kompakt, gut lesbar und jederzeit im Blick.

Ziel ist eine pragmatische Lösung für Büro, Werkstatt oder Hobbyraum: einschalten, hinschauen, fertig. Im weiteren Verlauf zeige ich dir Schritt für Schritt, wie die Daten abgeholt, sinnvoll benannt und fürs 16×2-Display aufbereitet werden – inklusive Script und Tipps für die Praxis.

air-Q pro - Livedaten via REST abrufen und auf LCDq1602 anzeigen lassen

Transparenz-Hinweis: Der air-Q pro wurde mir von der Corant GmbH kostenfrei zur Verfügung gestellt. Dieser Beitrag ist redaktionell unabhängig; es gab keine inhaltlichen Vorgaben und keine Bezahlung. Nach Veröffentlichung des letzten Beitrags zu dieser Reihe sende ich das Testgerät an die Corant GmbH zurück.

air-Q pro - Ansicht von vorn
air-Q pro – Ansicht von vorn
air-Q pro - Ansicht von Oben
air-Q pro – Ansicht von Oben
air-Q pro - Ansicht von Unten
air-Q pro – Ansicht von Unten

Inhaltsverzeichnis

    • Daten vom air-Q pro laden
    • Was bedeutet das konkret?
      • Schnelltest: Encrypted-Antwort ansehen
    • Minimalbeispiel (Python): Abrufen & Entschlüsseln
  • Messwerte ans MakerDisplay senden

Daten vom air-Q pro laden

Der air-Q pro stellt seine Messwerte lokal im Netzwerk bereit—typisch unter
http://<IP-Adresse>/data.
Der Haken: Die Antwort ist nicht direkt lesbar. Du bekommst ein kurzes JSON, dessen Feld content eine Base64-Zeichenkette enthält. Darin steckt wiederum ein AES-256-CBC-verschlüsseltes JSON mit allen Messwerten.

air-Q pro - verschlüsselte Messdaten
air-Q pro – verschlüsselte Messdaten

Was bedeutet das konkret?

  1. HTTP-GET an /data → JSON mit content.
  2. Base64 dekodieren → bekommst Bytes: IV || CIPHERTEXT.
  3. AES-256-CBC entschlüsseln
    • IV = die ersten 16 Bytes.
    • Key = aus deinem air-Q-Passwort erzeugt (UTF-8, auf 32 Bytes aufgefüllt/abgeschnitten).
  4. PKCS#7-Padding entfernen, Ergebnis als UTF-8 interpretieren.
  5. JSON parsen → jetzt hast du alle Felder (13 Messgrößen + 2 virtuelle Werte).

Warum verschlüsselt?
Der Hersteller schützt so die Daten selbst im lokalen Netz. Offiziell ist das nicht dokumentiert; naheliegend sind Datenschutz/Manipulationsschutz. Praktisch heißt das für uns: erst entschlüsseln, dann nutzen.

Schnelltest: Encrypted-Antwort ansehen

In einer PowerShell Konsole oder unter einem Linux Terminalfenster kann man sich mit curl recht einfach die Daten wie folgt laden: curl http://192.168.178.108/data

PowerShell 7.5.2
PS C:\Users\stefa> curl http://192.168.178.108/data
{"content":"mOcmtRiI43ER8vG1bytSba0UcqWTRJ1BdamyRrrXnfPquIjVXWtIb0f3yYagFGlx7Jx80jAZ...kBgM"}
PS C:\Users\stefa>

Minimalbeispiel (Python): Abrufen & Entschlüsseln

Voraussetzung: PyCryptodome ist installiert (pip install pycryptodome).
Der air-Q liefert die Daten unter http://<IP>/data. Das Feld content enthält Base64 + AES-256-CBC-verschlüsseltes JSON.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Minimalbeispiel: air-Q pro Daten laden und entschlüsseln.
Ergebnis: Vollständiges Messwerte-JSON auf der Konsole.
"""

import base64
import json
import http.client
from Crypto.Cipher import AES  # Paket: pycryptodome

AIRQ_IP = "192.168.178.108"   # <- anpassen
AIRQ_PASSWORT = "dein-passwort"  # <- anpassen
AES_SCHLUESSEL_LAENGE = 32
AES_BLOCKGROESSE = 16  # CBC-IV-Länge in Bytes

def _erzeuge_aes_schluessel(passwort: str) -> bytes:
    """Passwort als UTF-8 auf 32 Byte bringen (auffüllen/abschneiden)."""
    key = passwort.encode("utf-8")
    return key + b"0" * (AES_SCHLUESSEL_LAENGE - len(key)) if len(key) < AES_SCHLUESSEL_LAENGE else key[:AES_SCHLUESSEL_LAENGE]

def _entferne_pkcs7_padding(daten: bytes) -> bytes:
    """PKCS#7-Padding entfernen (letztes Byte = Anzahl der Füllbytes)."""
    return daten[:-daten[-1]]

def hole_rohantwort(ip_adresse: str) -> dict:
    """Holt das äußere JSON vom air-Q (mit dem Feld 'content')."""
    conn = http.client.HTTPConnection(ip_adresse, timeout=5)
    try:
        conn.request("GET", "/data")
        resp = conn.getresponse()
        return json.loads(resp.read())
    finally:
        conn.close()

def entschluessle_content(content_b64: str, passwort: str) -> dict:
    """Base64 → AES-256-CBC entschlüsseln → JSON als dict zurückgeben."""
    blob = base64.b64decode(content_b64)
    iv, ciphertext = blob[:AES_BLOCKGROESSE], blob[AES_BLOCKGROESSE:]
    cipher = AES.new(_erzeuge_aes_schluessel(passwort), AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    plaintext = _entferne_pkcs7_padding(plaintext)
    return json.loads(plaintext.decode("utf-8"))

if __name__ == "__main__":
    roh = hole_rohantwort(AIRQ_IP)
    daten = entschluessle_content(roh["content"], AIRQ_PASSWORT)
    print(json.dumps(daten, ensure_ascii=False, indent=2))

Auf der Konsole werden nun die entschlüsselten Messdaten im JSON-Format angezeigt.

air-Q pro - Messdaten im JSON-Format
air-Q pro – Messdaten im JSON-Format

Messwerte ans MakerDisplay senden

Nachdem wir die Daten erfolgreich empfangen und entschlüsselt haben, können wir sie entweder auf dem LCD1602 (MakerDisplay) anzeigen oder parallel als CSV lokal mitschreiben. Beides hat Vorteile:

  • Display: Live-Überblick, ohne App/Cloud.
  • CSV: Verlauf auswerten (z. B. in Excel).

Darum steuern wir das Verhalten per Parametern – und behalten ein einziges, sauberes Skript.

Hinweis: Du musst nur IP-Adresse deines air-Q und dein Geräte-Kennwort (Standard: airqsetup) eintragen. Für die Anzeige trägst du die URL deines MakerDisplays ein (z. B. http://192.168.178.96/anzeige).

fertiges Projekt – Messdaten des air-Q pro am LCD1602 Display visualisierenHerunterladen
air-Q-pro Messdaten am LCD1602 Display visualisieren
air-Q-pro Messdaten am LCD1602 Display visualisieren
Quellcode
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Titel: Luftqualität live: air-Q pro Daten via REST abrufen, entschlüsseln und weiterverarbeiten
Autor: Stefan Draeger
Blogartikel: https://draeger-it.blog/luftqualitaet-live-air-q-pro-daten-via-rest-abrufen-und-anzeigen/

Beschreibung:
Dieses Skript lädt Sensordaten vom air-Q pro, entschlüsselt sie (AES-256-CBC) und liefert ein
klar strukturiertes Objekt (AirQMessung) zurück. Die Anzeige/Weiterverarbeitung (CSV, LCD)
ist bewusst entkoppelt, damit du das Objekt flexibel verwenden kannst.
"""

from __future__ import annotations

import argparse
import base64
import csv
import http.client
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional
import urllib.request
from typing import Tuple

from Crypto.Cipher import AES  # pycryptodome

# --------------------------------------------------------------------
# Konfiguration (Defaults für CLI; produktiv per .env/Umgebungsvariablen)
# --------------------------------------------------------------------
STANDARD_AIRQ_IP = "192.168.178.108"
STANDARD_AIRQ_PASSWORT = "draeger1980"

AIRQ_HTTP_PORT = 80
AIRQ_HTTP_PFAD = "/data"
AIRQ_HTTP_TIMEOUT_S = 8
AIRQ_HTTP_RETRIES = 3

AES_SCHLUESSEL_LAENGE = 32
AES_BLOCKGROESSE = 16


# --------------------------------------------------------------------
# Domänenmodell
# --------------------------------------------------------------------
@dataclass(frozen=True)
class AirQMessung:
    """
    Repräsentiert eine Messung des air-Q.
    - zeitpunkt_local / zeitpunkt_utc: Zeitpunkt (aus 'timestamp' in ms)
    - device_id, uptime_s: Gerätemetadaten
    - rohwerte: Original-JSON (entschlüsselt)
    - werte: deutsch beschriftete, einheitenklare Basiswerte (für CSV/LCD)
    - status: Warmup-/Hinweistexte je Sensor
    """
    zeitpunkt_local: datetime
    zeitpunkt_utc: datetime
    device_id: Optional[str]
    uptime_s: Optional[int]
    rohwerte: Dict[str, Any]
    werte: Dict[str, Any]
    status: Dict[str, str]


# --------------------------------------------------------------------
# Datenabruf & Entschlüsselung
# --------------------------------------------------------------------
def _entferne_pkcs7_padding(daten: bytes) -> bytes:
    """Entfernt PKCS#7-Padding sicher im Byte-Kontext."""
    return daten[:-daten[-1]]

def _erzeuge_aes_schluessel(passwort: str) -> bytes:
    """Erzeugt einen 32-Byte-AES-Schlüssel aus dem Passwort (auffüllen/abschneiden)."""
    key = passwort.encode("utf-8")
    return key + b"0" * (AES_SCHLUESSEL_LAENGE - len(key)) if len(key) < AES_SCHLUESSEL_LAENGE else key[:AES_SCHLUESSEL_LAENGE]

def _entschluessle_content(base64_text: str, passwort: str) -> Dict[str, Any]:
    """Entschlüsselt den base64-kodierten 'content' des air-Q und liefert das JSON als Dict."""
    verschluesselt = base64.b64decode(base64_text)
    iv, payload = verschluesselt[:AES_BLOCKGROESSE], verschluesselt[AES_BLOCKGROESSE:]
    cipher = AES.new(key=_erzeuge_aes_schluessel(passwort), mode=AES.MODE_CBC, IV=iv)
    entschluesselt = cipher.decrypt(payload)
    json_text = _entferne_pkcs7_padding(entschluesselt).decode("utf-8")
    return json.loads(json_text)

def _hole_rohdaten(ip_adresse: str) -> Dict[str, Any]:
    """
    Holt das äußere JSON (mit 'content') fest über http://<ip>/data (Port 80).
    Baut Retries und aussagekräftige Fehler ein.
    """
    last_err: Optional[Exception] = None

    for attempt in range(1, AIRQ_HTTP_RETRIES + 1):
        conn = http.client.HTTPConnection(ip_adresse, port=AIRQ_HTTP_PORT, timeout=AIRQ_HTTP_TIMEOUT_S)
        try:
            conn.request("GET", AIRQ_HTTP_PFAD)
            resp = conn.getresponse()
            body = resp.read()
            if resp.status == 200:
                return json.loads(body)
            raise RuntimeError(f"HTTP {resp.status} {resp.reason} auf {AIRQ_HTTP_PFAD}")
        except (socket.timeout, TimeoutError, OSError, RuntimeError, json.JSONDecodeError) as e:
            last_err = e
            # kleiner Backoff zwischen den Versuchen
            time.sleep(0.5 * attempt)
        finally:
            try:
                conn.close()
            except Exception:
                pass

    raise ConnectionError(
        f"air-Q unter http://{ip_adresse}{AIRQ_HTTP_PFAD} nicht erreichbar (Port 80). "
        f"Letzter Fehler: {last_err}"
    )


def hole_airq_objekt(ip_adresse: str, passwort: str) -> Dict[str, Any]:
    """
    EINSTIEGSPUNKT für Datenzugriff:
    - Holt die Rohantwort
    - Entschlüsselt 'content'
    - Gibt das entschlüsselte JSON (als Dict) zurück
    """
    rohdaten = _hole_rohdaten(ip_adresse)
    return _entschluessle_content(rohdaten["content"], passwort)


# --------------------------------------------------------------------
# Normierung & Beschriftung
# --------------------------------------------------------------------
# Mapping: JSON-Key -> (deutscher Name, Einheit)
MAPPING: Dict[str, Tuple[str, Optional[str]]] = {
    "pm1":         ("PM1.0", "µg/m³"),
    "pm2_5":       ("PM2.5", "µg/m³"),
    "pm10":        ("PM10", "µg/m³"),
    "TypPS":       ("Ø Partikelgröße", "µm"),
    "dewpt":       ("Taupunkt", "°C"),
    "temperature": ("Temperatur", "°C"),
    "humidity":    ("Relative Luftfeuchte", "%"),
    "humidity_abs":("Absolute Luftfeuchte", "g/m³"),
    "pressure":    ("Luftdruck", "hPa"),
    "sound":       ("Lärmpegel", "dB(A)"),
    "sound_max":   ("Lärmpegel max (2 min)", "dB(A)"),
    "tvoc":        ("TVOC (flüchtige organische Verbindungen)", "ppb"),
    "co":          ("CO (Kohlenmonoxid)", "ppm"),     # korrigiert
    "co2":         ("CO₂ (Kohlendioxid)", "ppm"),     # korrigiert
    "health":      ("Gesundheitsindex", None),
    "performance": ("Leistungsindex", None),
    "dHdt":        ("Δ absolute LF", "mg/m³/s"),
    "dCO2dt":      ("Δ CO₂", "ppb/s"),
    "uptime":      ("Betriebszeit", "s"),
    "measuretime": ("Messdauer letzter Durchlauf", "ms"),
    "DeviceID":    ("Geräte-ID", None),
    "timestamp":   ("Zeitstempel (ms)", "ms"),
}

def _wert_aus_array_oder_skalar(v: Any) -> Tuple[Any, Optional[Any]]:
    """
    air-Q liefert häufig [wert, unsicherheit].
    Gibt (basiswert, unsicherheit) zurück.
    """
    if isinstance(v, (list, tuple)) and len(v) >= 1:
        basis = v[0]
        unsicherheit = v[1] if len(v) > 1 else None
        return basis, unsicherheit
    return v, None


def _normalisiere_status(raw) -> Dict[str, str]:
    """
    Wandelt das 'Status'-Feld in ein Dict[str, str] um.
    - Dict: Schlüssel/Werte als Strings
    - String/Number/sonstiges: {"status": "<wert>"}
    - Liste/Tuple: {"status_0": "...", "status_1": "...", ...}
    - None: {}
    """
    if raw is None:
        return {}
    if isinstance(raw, dict):
        return {str(k): str(v) for k, v in raw.items()}
    if isinstance(raw, (list, tuple)):
        return {f"status_{i}": str(v) for i, v in enumerate(raw)}
    # alles andere (z. B. "OK")
    return {"status": str(raw)}

def normiere_messung(entschluesselt: Dict[str, Any]) -> AirQMessung:
    """
    Wandelt das entschlüsselte Dict in eine AirQMessung:
    - Zeit aus 'timestamp' (ms) → UTC + lokale Zeit
    - deutsch beschriftete, einheitenklare Basiswerte
    - Statusmeldungen (Warmup etc.) unter 'Status'
    """
    # Zeitstempel (ms) → UTC/local
    ts_ms = entschluesselt.get("timestamp")
    if isinstance(ts_ms, (int, float)):
        ts_utc = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
    else:
        ts_utc = datetime.now(tz=timezone.utc)
    ts_local = ts_utc.astimezone()



    # Status-Hinweise
    status_norm = _normalisiere_status(entschluesselt.get("Status"))

    # Beschriftete Werte
    werte: Dict[str, Any] = {}
    for key, (name_de, einheit) in MAPPING.items():
        if key in entschluesselt:
            basiswert, _unsicherheit = _wert_aus_array_oder_skalar(entschluesselt[key])
            label = f"{name_de} ({einheit})" if einheit else name_de
            werte[label] = basiswert

    return AirQMessung(
        zeitpunkt_local=ts_local,
        zeitpunkt_utc=ts_utc,
        device_id=str(entschluesselt.get("DeviceID")) if entschluesselt.get("DeviceID") else None,
        uptime_s=int(entschluesselt.get("uptime")) if entschluesselt.get("uptime") is not None else None,
        rohwerte=entschluesselt,
        werte=werte,
        status=status_norm,
    )


# --------------------------------------------------------------------
# Weiterverarbeitung (CSV / LCD-Stub) – optional
# --------------------------------------------------------------------
def schreibe_csv_zeile(datei: Path, messung: AirQMessung, erstelle_header: bool = True) -> None:
    """
    Schreibt eine Zeile in eine CSV-Datei (Delimiter ';').
    - Erstellt optional beim ersten Mal einen Header.
    - 'zeitpunkt' wird als ISO-String vorangestellt.
    """
    datei.parent.mkdir(parents=True, exist_ok=True)
    spalten = ["zeitpunkt"] + sorted(messung.werte.keys())
    schreibe_header = (not datei.exists()) and erstelle_header

    with datei.open("a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=spalten, delimiter=";")
        if schreibe_header:
            writer.writeheader()
        zeile = {"zeitpunkt": messung.zeitpunkt_local.isoformat(timespec="seconds")}
        zeile.update({k: messung.werte.get(k, "") for k in spalten if k != "zeitpunkt"})
        writer.writerow(zeile)

# ------------------------------------------------------------
# 1) Anzeigezeilen aus Messung erzeugen
# ------------------------------------------------------------
def formatiere_lcd_zeilen(messung: AirQMessung) -> Tuple[str, str]:
    """
    Baut zwei Textzeilen für ein 16x2 LCD.
    Da dein ESP 'scroll' unterstützt, dürfen die Zeilen länger sein;
    wir halten sie trotzdem kompakt & gut lesbar.
    """
    # Werte robust lesen (falls mal etwas fehlt)
    co2  = messung.werte.get("CO₂ (Kohlendioxid) (ppm)")
    tvoc = messung.werte.get("TVOC (flüchtige organische Verbindungen) (ppb)")
    temp = messung.werte.get("Temperatur (°C)")
    rh   = messung.werte.get("Relative Luftfeuchte (%)")

    # Schonend runden/formatieren
    def fmt(v, nd=0):
        if v is None or v == "-":
            return "-"
        try:
            return f"{round(float(v), nd)}"
        except Exception:
            return str(v)

    co2_s  = fmt(co2, 0)
    tvoc_s = fmt(tvoc, 0)
    temp_s = fmt(temp, 1)
    rh_s   = fmt(rh, 1)

    # Zeile 1 & 2 kompakt – darf länger sein (scroll)
    zeile1 = f"CO2:{co2_s}ppm  TVOC:{tvoc_s}ppb"
    zeile2 = f"T:{temp_s}C  rH:{rh_s}%"

    return zeile1, zeile2


# ------------------------------------------------------------
# 2) HTTP-POST an den ESP-Endpoint senden
# ------------------------------------------------------------
def sende_an_lcd_http(lcd_url: str, zeile1: str, zeile2: str,
                      modus: str = "scroll", anzeigedauer: int = 15,
                      timeout_s: int = 5) -> None:
    """
    Sendet die gewünschten LCD-Zeilen an den ESP (JSON-POST).
    Erwartet einen Endpoint wie: http://192.168.178.96/anzeige
    """
    payload = {
        "zeile1": zeile1,
        "zeile2": zeile2,
        "modus": modus,                # z.B. "scroll", "static", ...
        "anzeigedauer": anzeigedauer   # Sekunden
    }
    daten = json.dumps(payload).encode("utf-8")

    req = urllib.request.Request(
        url=lcd_url,
        data=daten,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            # Antwort bei Bedarf auswerten:
            _ = resp.read()
    except HTTPError as e:
        # Hier gerne auf logging umstellen
        print(f"HTTP-Fehler beim Senden an LCD ({e.code}): {e.reason}")
    except URLError as e:
        print(f"Verbindungsfehler zum LCD: {e.reason}")
    except Exception as e:
        print(f"Unerwarteter Fehler beim LCD-POST: {e}")


# ------------------------------------------------------------
# 3) Öffentliche Anzeige-Funktion (ersetzt den Stub)
# ------------------------------------------------------------
def zeige_auf_lcd_http(messung: AirQMessung, lcd_url: str,
                       modus: str = "scroll", anzeigedauer: int = 15) -> None:
    """
    Erzeugt zwei Anzeigezeilen aus der Messung und sendet sie per HTTP an den ESP-LCD-Service.
    """
    zeile1, zeile2 = formatiere_lcd_zeilen(messung)
    print(f"[LCD→HTTP] {zeile1} || {zeile2}")  # kleine Laufzeit-Info
    sende_an_lcd_http(
        lcd_url=lcd_url,
        zeile1=zeile1,
        zeile2=zeile2,
        modus=modus,
        anzeigedauer=anzeigedauer
    )

# --- IMPORTS (ergänzen) ---
import time
import json
import urllib.request
from urllib.error import URLError, HTTPError
from typing import Tuple, List

# Falls noch nicht vorhanden, aus deinem Script:
# from Crypto.Cipher import AES
# class AirQMessung: ...
# hole_airq_objekt(...), normiere_messung(...)

# ------------------------------------------------------------
# Hilfen für LCD-Text
# ------------------------------------------------------------
def _wert_aus_array_oder_skalar(v):
    """Basiswert (und Unsicherheit) aus [wert, unsicherheit] oder Skalar extrahieren."""
    if isinstance(v, (list, tuple)) and v:
        basis = v[0]
        unsicherheit = v[1] if len(v) > 1 else None
        return basis, unsicherheit
    return v, None

def _fmt_wert(v, nd=0) -> str:
    """Zahl sauber formatieren; fällt robust auf str(v) zurück."""
    try:
        return f"{round(float(v), nd)}"
    except Exception:
        return str(v)

def _kuerze16(text: str) -> str:
    """Max 16 Zeichen fürs 16x2-LCD."""
    return text if len(text) <= 16 else text[:16]

def _baue_zeile(tag_kurz: str, wert, einheit_kurz: str = "", nd: int = 0) -> str:
    """
    Erzeugt eine LCD-Zeile wie 'CO2 1164ppm' bzw. 'T 23.4C'.
    Hält strikt 16 Zeichen ein.
    """
    val = _fmt_wert(wert, nd)
    unit = einheit_kurz or ""
    zeile = f"{tag_kurz} {val}{unit}"
    if len(zeile) > 16:
        zeile = f"{tag_kurz}:{val}{unit}"
    return _kuerze16(zeile)

# ------------------------------------------------------------
# HTTP-POST an den ESP-Endpoint
# ------------------------------------------------------------
def _sende_an_lcd_http(lcd_url: str, zeile1: str, zeile2: str,
                       modus: str = "static", anzeigedauer: int = 3, timeout_s: int = 5) -> None:
    """
    Sendet zwei Zeilen an dein LCD via HTTP-JSON.
    'modus' hier auf 'static' (keine Scroll-Animation).
    """
    payload = {
        "zeile1": zeile1,
        "zeile2": zeile2,
        "modus": modus,
        "anzeigedauer": anzeigedauer
    }
    daten = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url=lcd_url,
        data=daten,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            _ = resp.read()
    except HTTPError as e:
        print(f"HTTP-Fehler LCD ({e.code}): {e.reason}")
    except URLError as e:
        print(f"Verbindungsfehler LCD: {e.reason}")
    except Exception as e:
        print(f"Unerwarteter LCD-Fehler: {e}")

# ------------------------------------------------------------
# 1) Eine Messung -> Seiten (2 Zeilen) nacheinander anzeigen
# ------------------------------------------------------------
def zeige_auf_lcd_http_sequenziell(messung: AirQMessung, lcd_url: str, warte_sek: int = 3) -> None:
    """
    Zeigt aus *einer* Messung nacheinander alle relevanten Werte an.
    Jede Seite: 2 Zeilen à max. 16 Zeichen, danach 'warte_sek' warten.
    """
    roh = messung.rohwerte

    # Reihenfolge & Formatregeln (key, tag, unit, decimals)
    anzeigen_reihenfolge = [
        ("co2",        "CO2",   "ppm", 0),
        ("tvoc",       "TVOC",  "ppb", 0),
        ("temperature","T",     "C",   1),
        ("humidity",   "rH",    "%",   1),
        ("pressure",   "p",     "hPa", 1),
        ("pm1",        "PM1",   "ug/m3", 0),
        ("pm2_5",      "PM2.5", "ug/m3", 0),
        ("pm10",       "PM10",  "ug/m3", 0),
        ("co",         "CO",    "ppm", 2),
        ("sound",      "dB",    "",    1),
        ("sound_max",  "dBmax", "",    1),
        ("dewpt",      "Td",    "C",   1),
        ("humidity_abs","AbsLF","g/m3",1),
        ("dCO2dt",     "dCO2",  "ppb/s", 0),
        ("dHdt",       "dH",    "mg/m3/s", 1),
        ("health",     "Health","",    0),
        ("performance","Perf",  "",    0),
    ]

    # Alle vorhandenen Messzeilen generieren
    zeilen: List[str] = []
    for key, tag, unit, nd in anzeigen_reihenfolge:
        if key in roh:
            basis, _ = _wert_aus_array_oder_skalar(roh[key])
            zeilen.append(_baue_zeile(tag, basis, unit, nd))

    if not zeilen:
        # Fallback
        _sende_an_lcd_http(lcd_url, "Keine Daten", "", anzeigedauer=warte_sek)
        time.sleep(warte_sek)
        return

    # In 2er-Pärchen (Seiten) aufteilen
    for i in range(0, len(zeilen), 2):
        z1 = zeilen[i]
        z2 = zeilen[i+1] if i + 1 < len(zeilen) else ""
        # Senden & warten
        _sende_an_lcd_http(lcd_url, z1, z2, modus="static", anzeigedauer=warte_sek)
        time.sleep(warte_sek)

# ------------------------------------------------------------
# 2) Endlosschleife: Daten holen -> sequenziell anzeigen -> wiederholen
# ------------------------------------------------------------
def starte_lcd_endlosschleife(ip: str, passwort: str, lcd_url: str,
                              warte_sek: int = 3, pause_nach_zyklus: int = 0) -> None:
    """
    Holt in einer Endlosschleife neue Messungen und zeigt diese sequenziell an.
    - 'warte_sek': Wartezeit zwischen den Seiten
    - 'pause_nach_zyklus': zusätzliche Pause nach *allen* Seiten (optional)
    """
    while True:
        try:
            entschluesselt = hole_airq_objekt(ip, passwort)
            messung = normiere_messung(entschluesselt)
            zeige_auf_lcd_http_sequenziell(messung, lcd_url, warte_sek=warte_sek)
        except Exception as e:
            print(f"Fehler im Anzeigezyklus: {e}")
            # kurze Atempause, damit bei Dauerfehlern nicht gespammt wird
            time.sleep(max(5, warte_sek))
        if pause_nach_zyklus > 0:
            time.sleep(pause_nach_zyklus)


# --------------------------------------------------------------------
# CLI / main
# --------------------------------------------------------------------
def main() -> None:
    """
    Nutzung:
      # nur einmal laden & anzeigen
      python main.py

      # CSV schreiben
      python main.py --csv daten/airq.csv

      # LCD-Stub anzeigen
      python main.py --lcd

      # beides (CSV + LCD)
      python main.py --csv daten/airq.csv --lcd
    """
    parser = argparse.ArgumentParser(description="air-Q Daten abrufen, entschlüsseln und verarbeiten.")
    parser.add_argument("--ip", default=STANDARD_AIRQ_IP, help="IP-Adresse des air-Q (Default: %(default)s)")
    parser.add_argument("--passwort", default=STANDARD_AIRQ_PASSWORT, help="Passwort des air-Q")
    parser.add_argument("--csv", type=Path, help="Pfad zur CSV-Datei (schreibt eine Zeile)")
    parser.add_argument("--lcd-url", help="HTTP-Endpoint des ESP-LCD (z. B. http://192.168.178.96/anzeige)")
    parser.add_argument("--lcd-warte", type=int, default=3, help="Wartezeit je Seite in Sekunden (Default: %(default)s)")
    parser.add_argument("--lcd-loop", action="store_true", help="Endlosschleife: Messung holen, anzeigen, wiederholen")
    parser.add_argument("--loop-pause", type=int, default=0, help="Zusatzpause nach jedem kompletten Anzeigenzyklus (s)")

    args = parser.parse_args()

    # 1) Rohobjekt laden (entschlüsselte JSON als Dict)
    entschluesselt = hole_airq_objekt(args.ip, args.passwort)

    # 2) Normieren in Domänenobjekt
    messung = normiere_messung(entschluesselt)



    # 3) Ohne Flags: kurze Übersicht
    if not args.csv and not args.lcd_url:
        print(f"[{messung.zeitpunkt_local:%Y-%m-%d %H:%M:%S}] air-Q Messwerte")
        for k in sorted(messung.werte.keys()):
            print(f"  - {k}: {messung.werte[k]}")
        if messung.status:
            print("\nStatus:")
            for sensor, text in messung.status.items():
                print(f"  - {sensor}: {text}")
        return

    # 4) CSV
    if args.csv:
        schreibe_csv_zeile(args.csv, messung)
        print(f"CSV aktualisiert: {args.csv}")

    # 5) LCD
    if args.lcd_url and args.lcd_loop:
        starte_lcd_endlosschleife(args.ip, args.passwort, args.lcd_url,
                                  warte_sek=args.lcd_warte,
                                  pause_nach_zyklus=args.loop_pause)
        return

    if args.lcd_url:
        # Nur *eine* Messung holen und sequenziell anzeigen
        entschluesselt = hole_airq_objekt(args.ip, args.passwort)
        messung = normiere_messung(entschluesselt)
        zeige_auf_lcd_http_sequenziell(messung, args.lcd_url, warte_sek=args.lcd_warte)
        return


if __name__ == "__main__":
    main()

Schreibe einen Kommentar Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Fragen oder Feedback?

Du hast eine Idee, brauchst Hilfe oder möchtest Feedback loswerden?
Support-Ticket erstellen

Newsletter abonnieren

Bleib auf dem Laufenden: Erhalte regelmäßig Updates zu neuen Projekten, Tutorials und Tipps rund um Arduino, ESP32 und mehr – direkt in dein Postfach.

Jetzt Newsletter abonnieren

Unterstütze meinen Blog

Wenn dir meine Inhalte gefallen, freue ich mich über deine Unterstützung auf Tipeee.
So hilfst du mit, den Blog am Leben zu halten und neue Beiträge zu ermöglichen.

draeger-it.blog auf Tipeee unterstützen

Vielen Dank für deinen Support!
– Stefan Draeger

Kategorien

Tools

  • Unix-Zeitstempel-Rechner
  • ASCII Tabelle
  • Spannung, Strom, Widerstand und Leistung berechnen
  • Widerstandsrechner
  • 8×8 LED Matrix Tool
  • 8×16 LED Matrix Modul von Keyestudio
  • 16×16 LED Matrix – Generator

Links

Blogverzeichnis Bloggerei.de TopBlogs.de das Original - Blogverzeichnis | Blog Top Liste Blogverzeichnis trusted-blogs.com

Stefan Draeger
Königsberger Str. 13
38364 Schöningen
Tel.: 01778501273
E-Mail: info@draeger-it.blog

Folge mir auf

link zu Fabook
link zu LinkedIn
link zu YouTube
link zu TikTok
link zu Pinterest
link zu Instagram
  • Impressum
  • Datenschutzerklärung
  • Disclaimer
  • Cookie-Richtlinie (EU)
©2025 Technik Blog | Built using WordPress and Responsive Blogily theme by Superb
Cookie-Zustimmung verwalten
Wir verwenden Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wir tun dies, um das Surferlebnis zu verbessern und um personalisierte Werbung anzuzeigen. Wenn Sie diesen Technologien zustimmen, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn Sie Ihre Zustimmung nicht erteilen oder zurückziehen, können bestimmte Funktionen beeinträchtigt werden.
Funktional Immer aktiv
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Vorlieben
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Statistiken
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt. Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Marketing
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.
Optionen verwalten Dienste verwalten Verwalten von {vendor_count}-Lieferanten Lese mehr über diese Zwecke
Einstellungen anzeigen
{title} {title} {title}