"""Einstellungen aus der Web-App holen. Bis hierher stand die Konfiguration des Detectors auf seiner Kommandozeile — Stream-Adresse, Schwellen, Lebensräume. Wer die Kamera umstellen wollte, brauchte eine Shell auf dem Rechner, auf dem der Detector läuft. Für ein Werkzeug, das andere Leute benutzen sollen, ist das die falsche Stelle. Jetzt fragt der Detector die Web-App: ``GET /api/config/`` mit seinem Token, und bekommt zurück, was für diese Kamera gilt — samt Artenliste, denn welche Arten überhaupt zählen, ist eine inhaltliche Entscheidung und gehört in den Admin, nicht in eine Python-Datei. Zwei Eigenschaften sind wichtiger als die Funktion selbst: * **Die Kommandozeile bleibt gültig.** Was dort steht, gilt weiter, solange die Web-App nichts anderes sagt. Ein Detector muss auch ohne erreichbare Web-App starten können. * **Ein Ausfall ändert nichts.** Antwortet die Web-App nicht, läuft die Erkennung mit dem letzten bekannten Stand weiter, statt stehenzubleiben oder auf Vorgaben zurückzufallen. """ from __future__ import annotations import json import time import urllib.error import urllib.request from .species_de import Species def arten_aus_json(rohe: list[dict]) -> list[Species]: """Die Artenliste der Web-App in Katalogeinträge übersetzen. Der wissenschaftliche Name ist der Schlüssel ins Label-File des Modells; alles andere ist Beschriftung. Einträge ohne wissenschaftlichen Namen sind wertlos und fliegen raus, statt später leise nichts zu treffen. """ arten: list[Species] = [] for a in rohe: sci = (a.get("scientific") or "").strip() if not sci: continue arten.append( Species( scientific=sci, german=a.get("german") or sci, english=a.get("english") or "", rarity=a.get("rarity") or "haeufig", fact="", size_cm="", lebensraum=a.get("lebensraum") or "garten", ) ) return arten class Fernkonfig: """Holt die Konfiguration und sagt, wann es sich lohnt, wieder zu fragen.""" def __init__(self, api: str, token: str, intervall: float = 300.0, timeout: int = 10): self.url = api.rstrip("/") + "/api/config/" self.token = token self.intervall = intervall self.timeout = timeout self.letzte: dict | None = None self._letzter_versuch: float | None = None self._fehler_gemeldet = False # -- Abruf ------------------------------------------------------------ def faellig(self, jetzt: float) -> bool: return ( self._letzter_versuch is None or jetzt - self._letzter_versuch >= self.intervall ) def hole(self, jetzt: float | None = None) -> dict | None: """Einmal fragen. ``None`` heißt: nicht erreichbar oder unbrauchbar. Der Aufrufer behandelt ``None`` als "nichts ändert sich" — nicht als Fehler, der den Betrieb anhält. """ self._letzter_versuch = time.monotonic() if jetzt is None else jetzt req = urllib.request.Request( self.url, headers={ "Authorization": f"Bearer {self.token}", "User-Agent": "vogelalbum-detector/1.0", }, ) try: with urllib.request.urlopen(req, timeout=self.timeout) as resp: daten = json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: # 401 ist kein Netzproblem, sondern ein falscher Token — das soll # man sehen, aber es darf die Erkennung nicht anhalten. self._melde(f"Konfiguration abgelehnt (HTTP {exc.code})") return None except (OSError, ValueError) as exc: self._melde(f"Konfiguration nicht erreichbar ({exc})") return None if not isinstance(daten, dict): self._melde("Konfiguration unlesbar (kein Objekt)") return None if self._fehler_gemeldet: print("[konfig] Web-App wieder erreichbar") self._fehler_gemeldet = False self.letzte = daten return daten def _melde(self, text: str) -> None: """Einmal warnen, nicht in jeder Runde — sonst ersäuft das Log.""" if not self._fehler_gemeldet: print(f"[konfig] {text} — weiter mit dem bisherigen Stand") self._fehler_gemeldet = True