feat: add alarm_thresholds to pegel detail endpoint
Scrape the official alarm level thresholds (Richtwasserstände) from the
station detail page and include them in GET /api/pegel/{pegel_id} as
alarm_thresholds, cached for 24 hours with a stale-cache fallback on
errors. null for stations without a flood alert function.
Also clarify in the flood_alert_level docs that 'Kein Hochwassermeldepegel'
means the station is not used for official flood alerts/control, not
that measurement data is missing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
489a026b9c
commit
b9cee0a7fb
2 files changed
+91
-6
No files matched your search
@@ -101,6 +101,12 @@ Beispielantwort:
|
|||||||
"flood_alert_level": "Kein Hochwasser"
|
"flood_alert_level": "Kein Hochwasser"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"alarm_thresholds": [
|
||||||
|
{ "level": 1, "water_level_cm": 360 },
|
||||||
|
{ "level": 2, "water_level_cm": 400 },
|
||||||
|
{ "level": 3, "water_level_cm": 480 },
|
||||||
|
{ "level": 4, "water_level_cm": 600 }
|
||||||
|
],
|
||||||
"source": "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/feed/wasserstand-pegel-560021",
|
"source": "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/feed/wasserstand-pegel-560021",
|
||||||
"fetched_at": "2026-05-05T00:16:00+00:00"
|
"fetched_at": "2026-05-05T00:16:00+00:00"
|
||||||
}
|
}
|
||||||
@@ -118,6 +124,7 @@ Beispielantwort:
|
|||||||
| `water_level_cm` | number oder `null` | Wasserstand in Zentimetern |
|
| `water_level_cm` | number oder `null` | Wasserstand in Zentimetern |
|
||||||
| `flow_m3s` | number oder `null` | Durchfluss in Kubikmetern pro Sekunde |
|
| `flow_m3s` | number oder `null` | Durchfluss in Kubikmetern pro Sekunde |
|
||||||
| `flood_alert_level` | string oder `null` | Meldestufe als Text, siehe unten |
|
| `flood_alert_level` | string oder `null` | Meldestufe als Text, siehe unten |
|
||||||
|
| `alarm_thresholds` | array oder `null` | Richtwasserstände der Alarmstufen 1–4, siehe unten |
|
||||||
| `source` | string | URL des verwendeten RSS-Feeds |
|
| `source` | string | URL des verwendeten RSS-Feeds |
|
||||||
| `fetched_at` | string | Zeitpunkt des Abrufs durch die API als UTC-Zeitstempel |
|
| `fetched_at` | string | Zeitpunkt des Abrufs durch die API als UTC-Zeitstempel |
|
||||||
|
|
||||||
@@ -131,11 +138,20 @@ Einträge ohne Wasserstand und ohne Durchfluss (Platzhalter der Quelle für noch
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `Niedrigwasser` | Niedrigwasser |
|
| `Niedrigwasser` | Niedrigwasser |
|
||||||
| `Kein Hochwasser` | Hochwassermeldepegel, Richtwerte nicht erreicht |
|
| `Kein Hochwasser` | Hochwassermeldepegel, Richtwerte nicht erreicht |
|
||||||
| `Kein Hochwassermeldepegel` | Pegel ohne Hochwassermeldefunktion |
|
| `Kein Hochwassermeldepegel` | Der Pegel wird nicht für amtliche Hochwassermeldungen bzw. -steuerung genutzt. Das bedeutet nicht, dass Messwerte fehlen; solche Pegel haben lediglich keine Alarmstufen und `alarm_thresholds` ist `null` |
|
||||||
| `Alarmstufe 1` bis `Alarmstufe 4` | Richtwert der jeweiligen Alarmstufe überschritten (1 Meldedienst, 2 Kontrolldienst, 3 Wachdienst, 4 Hochwasserabwehr) |
|
| `Alarmstufe 1` bis `Alarmstufe 4` | Richtwert der jeweiligen Alarmstufe überschritten (1 Meldedienst, 2 Kontrolldienst, 3 Wachdienst, 4 Hochwasserabwehr) |
|
||||||
|
|
||||||
Die ersten drei Werte wurden in den Feeds aller Pegel beobachtet. Die Alarmstufen stammen aus der Legende der HWIMS-Übersichtsseite; ihre genaue Schreibweise im RSS-Feed ist nicht verifiziert, da sie bei der Prüfung nicht aufgetreten sind. Clients sollten unbekannte Werte tolerieren.
|
Die ersten drei Werte wurden in den Feeds aller Pegel beobachtet. Die Alarmstufen stammen aus der Legende der HWIMS-Übersichtsseite; ihre genaue Schreibweise im RSS-Feed ist nicht verifiziert, da sie bei der Prüfung nicht aufgetreten sind. Clients sollten unbekannte Werte tolerieren.
|
||||||
|
|
||||||
|
### `alarm_thresholds`
|
||||||
|
|
||||||
|
Richtwasserstände (in cm), ab denen die jeweilige Alarmstufe laut HWIMS gilt. Die Werte werden von der Pegel-Detailseite (nicht dem RSS-Feed) ermittelt und 24 Stunden zwischengespeichert, da sie sich praktisch nie ändern. Ist die Detailseite nicht erreichbar, liefert die API einen vorhandenen (ggf. abgelaufenen) Cache-Wert oder `null`, ohne dass die übrige Antwort davon betroffen ist. Pegel ohne Hochwassermeldefunktion (`flood_alert_level` = `Kein Hochwassermeldepegel`) haben keine Richtwasserstände; hier ist das Feld `null`.
|
||||||
|
|
||||||
|
| Feld | Typ | Bedeutung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `level` | integer | Alarmstufe, 1 bis 4 |
|
||||||
|
| `water_level_cm` | integer | Richtwasserstand dieser Alarmstufe in Zentimetern |
|
||||||
|
|
||||||
## Fehlerantworten
|
## Fehlerantworten
|
||||||
|
|
||||||
Fehler der API enthalten ein JSON-Objekt mit dem Feld `detail`.
|
Fehler der API enthalten ein JSON-Objekt mit dem Feld `detail`.
|
||||||
@@ -207,6 +223,7 @@ Die Messwerte werden von der offiziellen Website des Freistaats Sachsen bereitge
|
|||||||
|
|
||||||
- Übersicht: <https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/wasserstand-uebersicht>
|
- Übersicht: <https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/wasserstand-uebersicht>
|
||||||
- Beispiel-Feed: <https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/feed/wasserstand-pegel-560021>
|
- Beispiel-Feed: <https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/feed/wasserstand-pegel-560021>
|
||||||
|
- Beispiel-Detailseite (Quelle für `alarm_thresholds`): <https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/wasserstand-pegel-560021>
|
||||||
|
|
||||||
Verfügbarkeit und Richtigkeit der Quelldaten werden nicht garantiert. Änderungen am Aufbau der HWIMS-Seiten oder RSS-Feeds können die Verarbeitung beeinträchtigen. Dieses Projekt ist nicht mit dem Freistaat Sachsen verbunden.
|
Verfügbarkeit und Richtigkeit der Quelldaten werden nicht garantiert. Änderungen am Aufbau der HWIMS-Seiten oder RSS-Feeds können die Verarbeitung beeinträchtigen. Dieses Projekt ist nicht mit dem Freistaat Sachsen verbunden.
|
||||||
|
|
||||||
|
|||||||
+73
-5
@@ -57,10 +57,11 @@ class PegelEntry(BaseModel):
|
|||||||
flood_alert_level: str | None = Field(
|
flood_alert_level: str | None = Field(
|
||||||
description=(
|
description=(
|
||||||
"Meldestufe als Text. Beobachtete Werte: `Niedrigwasser`, `Kein Hochwasser`, "
|
"Meldestufe als Text. Beobachtete Werte: `Niedrigwasser`, `Kein Hochwasser`, "
|
||||||
"`Kein Hochwassermeldepegel` (Pegel ohne Hochwassermeldefunktion). Bei "
|
"`Kein Hochwassermeldepegel` (der Pegel wird nicht für amtliche "
|
||||||
"Hochwasser sind laut HWIMS-Legende `Alarmstufe 1` bis `Alarmstufe 4` zu "
|
"Hochwassermeldungen bzw. -steuerung genutzt; für solche Pegel liefert "
|
||||||
"erwarten; deren genaue Schreibweise im Feed ist nicht verifiziert. "
|
"`alarm_thresholds` `null`). Bei Hochwasser sind laut HWIMS-Legende "
|
||||||
"Clients sollten unbekannte Werte tolerieren."
|
"`Alarmstufe 1` bis `Alarmstufe 4` zu erwarten; deren genaue Schreibweise "
|
||||||
|
"im Feed ist nicht verifiziert. Clients sollten unbekannte Werte tolerieren."
|
||||||
),
|
),
|
||||||
examples=[
|
examples=[
|
||||||
"Niedrigwasser",
|
"Niedrigwasser",
|
||||||
@@ -73,19 +74,38 @@ class PegelEntry(BaseModel):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class AlarmThreshold(BaseModel):
|
||||||
|
level: int = Field(description="Alarmstufe, 1 bis 4.")
|
||||||
|
water_level_cm: int = Field(description="Richtwasserstand dieser Alarmstufe in Zentimetern.")
|
||||||
|
|
||||||
class PegelResponse(BaseModel):
|
class PegelResponse(BaseModel):
|
||||||
station_id: str
|
station_id: str
|
||||||
station_title: str | None
|
station_title: str | None
|
||||||
count: int
|
count: int
|
||||||
latest: PegelEntry
|
latest: PegelEntry
|
||||||
entries: list[PegelEntry]
|
entries: list[PegelEntry]
|
||||||
|
alarm_thresholds: list[AlarmThreshold] | None = Field(
|
||||||
|
description=(
|
||||||
|
"Richtwasserstände der Alarmstufen 1 bis 4 in Zentimetern, von der "
|
||||||
|
"Pegel-Detailseite ermittelt. `null`, wenn der Pegel keine "
|
||||||
|
"Hochwassermeldefunktion hat (siehe `flood_alert_level`) oder die "
|
||||||
|
"Werte nicht ermittelt werden konnten."
|
||||||
|
)
|
||||||
|
)
|
||||||
source: str
|
source: str
|
||||||
fetched_at: str
|
fetched_at: str
|
||||||
|
|
||||||
BASE = "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/feed/wasserstand-pegel-{}"
|
BASE = "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/feed/wasserstand-pegel-{}"
|
||||||
|
DETAIL_BASE = "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/wasserstand-pegel-{}"
|
||||||
DISCOVERY_URL = "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/wasserstand-uebersicht"
|
DISCOVERY_URL = "https://www.umwelt.sachsen.de/umwelt/infosysteme/hwims/portal/web/wasserstand-uebersicht"
|
||||||
INDEX_CACHE_TTL_SECONDS = 900
|
INDEX_CACHE_TTL_SECONDS = 900
|
||||||
LATEST_CACHE_TTL_SECONDS = 120
|
LATEST_CACHE_TTL_SECONDS = 120
|
||||||
|
ALARM_THRESHOLDS_CACHE_TTL_SECONDS = 86400
|
||||||
|
|
||||||
|
ALARM_THRESHOLD_PATTERN = re.compile(
|
||||||
|
r'Alarmstufe\s*(\d)</span>.*?<td headers="rws">\s*([\d.,]+)\s*cm\s*</td>',
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
|
||||||
class IndexCache(TypedDict):
|
class IndexCache(TypedDict):
|
||||||
items: list[dict[str, Any]] | None
|
items: list[dict[str, Any]] | None
|
||||||
@@ -98,6 +118,7 @@ _index_cache: IndexCache = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_latest_cache: dict[str, dict[str, Any]] = {}
|
_latest_cache: dict[str, dict[str, Any]] = {}
|
||||||
|
_alarm_thresholds_cache: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
def parse_number(pattern, text):
|
def parse_number(pattern, text):
|
||||||
m = re.search(pattern, text, re.I)
|
m = re.search(pattern, text, re.I)
|
||||||
@@ -253,6 +274,48 @@ def _get_latest_for_station(station_id: str):
|
|||||||
}
|
}
|
||||||
return latest
|
return latest
|
||||||
|
|
||||||
|
def parse_alarm_thresholds(body: str):
|
||||||
|
# Pegel ohne Hochwassermeldefunktion ("Kein Hochwassermeldepegel") haben
|
||||||
|
# keine Richtwasserstände-Tabelle; in diesem Fall liefert das Muster
|
||||||
|
# keine Treffer und die Funktion gibt None zurück.
|
||||||
|
matches = ALARM_THRESHOLD_PATTERN.findall(body)
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
return [
|
||||||
|
{"level": int(level), "water_level_cm": round(float(value.replace(",", ".")))}
|
||||||
|
for level, value in matches
|
||||||
|
]
|
||||||
|
|
||||||
|
def _fetch_alarm_thresholds(station_id: str):
|
||||||
|
req = Request(
|
||||||
|
DETAIL_BASE.format(station_id),
|
||||||
|
headers={"User-Agent": "sachsen-pegel-proxy/1.0"},
|
||||||
|
)
|
||||||
|
with urlopen(req, timeout=12) as resp:
|
||||||
|
body = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
return parse_alarm_thresholds(body)
|
||||||
|
|
||||||
|
def _get_alarm_thresholds(station_id: str):
|
||||||
|
cache_hit = _alarm_thresholds_cache.get(station_id)
|
||||||
|
if cache_hit and _is_cache_fresh(
|
||||||
|
cache_hit.get("fetched_at"), ALARM_THRESHOLDS_CACHE_TTL_SECONDS
|
||||||
|
):
|
||||||
|
return cache_hit.get("thresholds")
|
||||||
|
|
||||||
|
try:
|
||||||
|
thresholds = _fetch_alarm_thresholds(station_id)
|
||||||
|
except (URLError, HTTPError, TimeoutError, OSError):
|
||||||
|
# Richtwasserstände sind ein optionales Zusatzfeld; bei Fehlern lieber
|
||||||
|
# auf einen vorhandenen (ggf. abgelaufenen) Cache zurückfallen oder
|
||||||
|
# `None` liefern, statt die ganze Anfrage scheitern zu lassen.
|
||||||
|
return cache_hit.get("thresholds") if cache_hit else None
|
||||||
|
|
||||||
|
_alarm_thresholds_cache[station_id] = {
|
||||||
|
"thresholds": thresholds,
|
||||||
|
"fetched_at": _now_utc(),
|
||||||
|
}
|
||||||
|
return thresholds
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/api/pegel",
|
"/api/pegel",
|
||||||
response_model=PegelIndexResponse,
|
response_model=PegelIndexResponse,
|
||||||
@@ -329,7 +392,11 @@ def list_pegel(limit: int | None = None):
|
|||||||
"`latest` ist der neueste verbleibende Eintrag und entspricht `entries[0]`; "
|
"`latest` ist der neueste verbleibende Eintrag und entspricht `entries[0]`; "
|
||||||
"die Reihenfolge von `entries` folgt dem Feed (neueste zuerst). "
|
"die Reihenfolge von `entries` folgt dem Feed (neueste zuerst). "
|
||||||
"Pegel-IDs bestehen aus mindestens sechs Ziffern. Einzelne fehlende "
|
"Pegel-IDs bestehen aus mindestens sechs Ziffern. Einzelne fehlende "
|
||||||
"Messwerte werden als `null` ausgegeben."
|
"Messwerte werden als `null` ausgegeben. `alarm_thresholds` wird "
|
||||||
|
"zusätzlich von der Pegel-Detailseite ermittelt und 24 Stunden "
|
||||||
|
"zwischengespeichert; ist sie nicht erreichbar, wird ein vorhandener "
|
||||||
|
"Cache verwendet oder `null` geliefert, ohne die übrige Antwort zu "
|
||||||
|
"beeinträchtigen."
|
||||||
),
|
),
|
||||||
response_description="Stationsmetadaten, neuester Messwert und Feed-Historie.",
|
response_description="Stationsmetadaten, neuester Messwert und Feed-Historie.",
|
||||||
responses={
|
responses={
|
||||||
@@ -358,6 +425,7 @@ def get_pegel(pegel_id: str):
|
|||||||
"count": len(entries),
|
"count": len(entries),
|
||||||
"latest": entries[0],
|
"latest": entries[0],
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
|
"alarm_thresholds": _get_alarm_thresholds(pegel_id),
|
||||||
"source": url,
|
"source": url,
|
||||||
"fetched_at": dt.datetime.now(dt.UTC).isoformat(),
|
"fetched_at": dt.datetime.now(dt.UTC).isoformat(),
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user