"""Meldungen über ntfy — Sichtungen und Störungen. Was hier bewusst NICHT passiert: die Anwendung aufhalten. Eine Meldung ist eine Nebensache; wenn der ntfy-Server nicht antwortet, darf das weder die Antwort an den Detector verzögern noch einen Fehler auslösen. Eine verpasste Meldung ist ärgerlich, eine verlorene Sichtung wäre schlimmer. """ from __future__ import annotations import logging import threading import urllib.error import urllib.request from .models import Benachrichtigung log = logging.getLogger(__name__) ZEITGRENZE = 8 # Sekunden def _senden(ziel: str, titel: str, text: str, prioritaet: str, symbole: str): """Schickt die Meldung. Läuft im eigenen Faden, siehe _im_hintergrund.""" try: anfrage = urllib.request.Request( ziel, data=text.encode("utf-8"), headers={ "Title": titel.encode("utf-8"), "Priority": prioritaet, "Tags": symbole, }, method="POST", ) with urllib.request.urlopen(anfrage, timeout=ZEITGRENZE): pass except (urllib.error.URLError, OSError, ValueError) as fehler: # Nur protokollieren. Wer eine Meldung nicht zustellen kann, soll # deshalb nicht den Betrieb anhalten. log.warning("ntfy nicht erreicht: %s", fehler) def _im_hintergrund(*args): threading.Thread(target=_senden, args=args, daemon=True).start() def melde_sichtung(sighting, ist_neue_art: bool) -> None: """Meldet eine Sichtung, je nach Einstellung im Backend.""" e = Benachrichtigung.laden() if not e.aktiv or not e.ziel: return if e.sichtungen == Benachrichtigung.Sichtungen.AUS: return if e.sichtungen == Benachrichtigung.Sichtungen.NEUE and not ist_neue_art: return art = sighting.species.german if sighting.species else "Unbestimmt" kamera = sighting.camera.name if sighting.camera else "unbekannte Kamera" sicherheit = round(sighting.confidence * 100) if ist_neue_art: titel = f"Neue Art: {art}" text = f"{kamera} · {sicherheit} % · zum ersten Mal gesehen" symbole = "sparkles" prioritaet = "default" else: titel = art text = f"{kamera} · {sicherheit} %" symbole = "bird" # Leise: Wer alle Sichtungen meldet, will nicht bei jedem Spatz # ein Summen am Handy. prioritaet = "low" _im_hintergrund(e.ziel, titel, text, prioritaet, symbole) def melde_stoerung(titel: str, text: str) -> None: """Meldet eine Störung — laut, weil sie Handeln erfordert. 🔑 **Synchron, im Gegensatz zu den Sichtungen.** Und das ist keine Unachtsamkeit, sondern der Grund, warum diese Meldung am 24.09.2026 nie ankam: Störungen meldet ausschließlich der Befehl ``stoerungen_pruefen``, und der ist ein Einmal-Lauf. Ein Hintergrundfaden ist dort ein Daemon-Faden — der stirbt, sobald der Prozess endet. Der Befehl war mit seiner Arbeit fertig, bevor die Meldung draußen war, und hat sie damit mitgenommen. ⚠️ Bei ``melde_sichtung`` ist der Hintergrundfaden richtig: Die läuft in einer Web-Anfrage, die weiterlebt, und darf den Detector nicht warten lassen. Hier gibt es nichts, was wartet -- das Senden IST die Arbeit. """ e = Benachrichtigung.laden() if not e.aktiv or not e.stoerungen or not e.ziel: return _senden(e.ziel, titel, text, "high", "rotating_light") def melde_probe() -> tuple[bool, str]: """Sofort und synchron — für den Knopf im Backend. Hier wird ausnahmsweise gewartet: Wer auf 'Probe senden' drückt, will wissen, ob es geklappt hat. Eine Meldung, die still im Hintergrund scheitert, wäre genau das Gegenteil einer Probe. """ e = Benachrichtigung.laden() if not e.ziel: return False, "Kein Thema eingetragen." try: anfrage = urllib.request.Request( e.ziel, data="Wenn du das liest, funktioniert es.".encode("utf-8"), headers={ "Title": "Vogelalbum Probe".encode("utf-8"), "Priority": "default", "Tags": "white_check_mark", }, method="POST", ) with urllib.request.urlopen(anfrage, timeout=ZEITGRENZE) as antwort: return True, f"Gesendet (HTTP {antwort.status})." except urllib.error.HTTPError as fehler: return False, f"Server antwortet mit {fehler.code}." except (urllib.error.URLError, OSError, ValueError) as fehler: return False, f"Nicht erreicht: {fehler}"