import "server-only"; import { env } from "@/lib/env"; /** * Wetterwarnungen des Deutschen Wetterdienstes. * * Das ist die **einzige** Verbindung, die FLORIAN von sich aus nach außen * aufbaut — und sie ist standardmäßig aus. Wer sie einschaltet, sollte wissen, * was dabei passiert: * * * Es geht eine Anfrage an opendata-Server des DWD, mehr nicht. Keine * Kennung, keine Daten der Wehr, kein Standort außer der Warnzellen-Nummer, * die in der Konfiguration steht. * * Die Antwort wird für ein paar Minuten zwischengespeichert, damit ein * Hallenmonitor, der jede Minute nachfragt, nicht jedes Mal hinausfunkt. * * Fällt die Abfrage aus, bleibt die Kachel leer. Eine Wetterwarnung ist * eine Zugabe; die Alarmierung läuft über die Leitstelle. * * Die Warnzellen-Nummer der eigenen Gemeinde steht in der Warnzellen-Liste des * DWD (`DWD_WARNCELL`). Ohne sie werden alle Warnungen ignoriert. */ export interface Wetterwarnung { /** 1 = Wetterwarnung, 2 = markantes Wetter, 3 = Unwetter, 4 = extremes Unwetter. */ stufe: number; ereignis: string; ueberschrift: string; beschreibung: string; von: Date | null; bis: Date | null; } export interface WetterAnbieter { warnungen(): Promise; verfuegbar(): boolean; } /** Ohne Konfiguration: keine Warnungen, kein Fehler. */ class KeineWarnungen implements WetterAnbieter { verfuegbar(): boolean { return false; } async warnungen(): Promise { return []; } } interface DwdWarnung { level?: number; event?: string; headline?: string; description?: string; start?: number; end?: number | null; } /** * Der DWD liefert seine Warnungen als JavaScript-Aufruf aus, nicht als reines * JSON: `warnWetter.loadWarnings({...});`. Die Klammer außen muss weg, bevor * sich der Inhalt lesen lässt. */ function jsonAusJsonp(text: string): unknown { const anfang = text.indexOf("("); const ende = text.lastIndexOf(")"); if (anfang < 0 || ende <= anfang) return JSON.parse(text); return JSON.parse(text.slice(anfang + 1, ende)); } class DwdWarnapp implements WetterAnbieter { /** Antwort und Zeitpunkt — der Monitor fragt oft, der DWD soll das nicht merken. */ private zwischenspeicher: { zeit: number; warnungen: Wetterwarnung[] } | null = null; verfuegbar(): boolean { return Boolean(env.DWD_WARNCELL); } async warnungen(): Promise { if (!this.verfuegbar()) return []; const alter = this.zwischenspeicher ? Date.now() - this.zwischenspeicher.zeit : Infinity; if (this.zwischenspeicher && alter < env.WETTER_ZWISCHENSPEICHER * 1000) { return this.zwischenspeicher.warnungen; } try { const antwort = await fetch(env.DWD_WARNUNGEN_URL, { signal: AbortSignal.timeout(env.WETTER_TIMEOUT * 1000), headers: { "User-Agent": "FLORIAN Feuerwehrverwaltung" }, cache: "no-store", }); if (!antwort.ok) throw new Error(`DWD antwortete mit ${antwort.status}.`); const daten = jsonAusJsonp(await antwort.text()) as { warnings?: Record; vorabInformation?: Record; }; const zelle = env.DWD_WARNCELL!; const roh = [...(daten.warnings?.[zelle] ?? []), ...(daten.vorabInformation?.[zelle] ?? [])]; const warnungen = roh.map((warnung) => ({ stufe: warnung.level ?? 1, ereignis: warnung.event ?? "Wetterwarnung", ueberschrift: warnung.headline ?? "", beschreibung: warnung.description ?? "", von: warnung.start ? new Date(warnung.start) : null, bis: warnung.end ? new Date(warnung.end) : null, })); this.zwischenspeicher = { zeit: Date.now(), warnungen }; return warnungen; } catch (fehler) { console.error("[wetter] Warnungen nicht abrufbar:", fehler); // Lieber den alten Stand als gar nichts — eine Unwetterwarnung von vor // zehn Minuten ist immer noch die richtige Information. return this.zwischenspeicher?.warnungen ?? []; } } } let anbieterCache: WetterAnbieter | null = null; export function wetter(): WetterAnbieter { if (!anbieterCache) { anbieterCache = env.WETTER_ENABLED ? new DwdWarnapp() : new KeineWarnungen(); } return anbieterCache; } /** Nur für Tests. */ export function _wetterErsetzen(ersatz: WetterAnbieter | null): void { anbieterCache = ersatz; } /** * Die Beschriftung der Warnstufen steht in wetter-texte.ts — der Hallenmonitor * ist eine Client-Komponente und dürfte dieses "server-only"-Modul nicht * anfassen. */ export { WARNSTUFE_TEXT } from "@/lib/integrations/wetter-texte";