Files
wetter/app.py
SimolZimol 2a9882c0aa new file: .dockerignore
new file:   Dockerfile
	new file:   __pycache__/app.cpython-310.pyc
	new file:   app.py
	new file:   requirements.txt
	new file:   static/css/style.css
	new file:   templates/base.html
	new file:   templates/index.html
	new file:   templates/weather.html
2026-04-21 09:01:53 +02:00

287 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
from flask import Flask, render_template, request, jsonify
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut
import math
import traceback
import pandas as pd
from wetterdienst.provider.dwd.mosmix import DwdMosmixRequest
app = Flask(__name__)
# ── Parameter (wetterdienst ≥ 0.100, Format: resolution/dataset/parameter) ───
MOSMIX_PARAMS = [
"hourly/small/temperature_air_mean_2m",
"hourly/small/wind_speed",
"hourly/small/wind_direction",
"hourly/small/wind_gust_max_last_1h",
"hourly/small/cloud_cover_total",
"hourly/small/pressure_air_site_reduced",
"hourly/small/precipitation_height_significant_weather_last_1h",
"hourly/small/sunshine_duration",
"hourly/large/precipitation_height_last_1h",
"hourly/large/probability_precipitation_height_gt_0_1mm_last_1h",
]
# ── Hilfsfunktionen ──────────────────────────────────────────────────────────
def geocode_location(query: str):
geolocator = Nominatim(user_agent="dwd-wetter-app/1.0")
try:
loc = geolocator.geocode(query, language="de", timeout=10)
if loc:
return loc.latitude, loc.longitude, loc.address
except GeocoderTimedOut:
pass
return None, None, None
def haversine(lat1, lon1, lat2, lon2):
R = 6371
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) ** 2
+ math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
* math.sin(dlon / 2) ** 2)
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def _isnan(v):
try:
return math.isnan(float(v))
except (TypeError, ValueError):
return True
def _round_temp(k):
"""Wetterdienst gives temperature already in degC."""
if k is None or _isnan(k):
return None
return round(float(k), 1)
def get_mosmix_forecast(lat, lon, hours=72):
"""Holt MOSMIX-Vorhersage für die nächsten Stunden."""
try:
req = DwdMosmixRequest(parameters=MOSMIX_PARAMS)
nearest = req.filter_by_rank(latlon=(lat, lon), rank=1)
result = nearest.values.all()
df = result.df
if df is None or (hasattr(df, "__len__") and len(df) == 0):
return [], {}
# Polars → Pandas
if hasattr(df, "to_pandas"):
df = df.to_pandas()
station_info = {}
sdf = nearest.df
if sdf is not None and len(sdf) > 0:
if hasattr(sdf, "to_pandas"):
sdf = sdf.to_pandas()
station_info = sdf.iloc[0].to_dict()
df = df.sort_values("date").copy()
# Auf die nächsten `hours` Stunden begrenzen
min_date = df["date"].min()
cutoff = min_date + pd.Timedelta(hours=hours)
df = df[df["date"] <= cutoff]
forecast = []
for date_val, group in df.groupby("date"):
params = {row["parameter"]: row["value"] for _, row in group.iterrows()}
temp_c = _round_temp(params.get("temperature_air_mean_2m"))
ff = params.get("wind_speed")
wind_kmh = round(float(ff) * 3.6, 1) if not _isnan(ff) else None
fx1 = params.get("wind_gust_max_last_1h")
gust_kmh = round(float(fx1) * 3.6, 1) if not _isnan(fx1) else None
pppp = params.get("pressure_air_site_reduced")
pressure = round(float(pppp), 1) if not _isnan(pppp) else None
# Niederschlag: rr1c (significant) oder rr1 (gesamt) als Fallback
rr1c = params.get("precipitation_height_significant_weather_last_1h")
rr1 = params.get("precipitation_height_last_1h")
precip_raw = rr1c if not _isnan(rr1c) else (rr1 if not _isnan(rr1) else None)
precip = round(float(precip_raw), 1) if precip_raw is not None else 0.0
# Regenwahrscheinlichkeit
rprob_raw = params.get("probability_precipitation_height_gt_0_1mm_last_1h")
rain_prob = round(float(rprob_raw)) if not _isnan(rprob_raw) else None
n = params.get("cloud_cover_total")
clouds = round(float(n)) if not _isnan(n) else None
sun = params.get("sunshine_duration")
sun_min = round(float(sun) / 60) if not _isnan(sun) else 0
wind_dir_v = params.get("wind_direction")
wind_dir = float(wind_dir_v) if not _isnan(wind_dir_v) else None
forecast.append({
"datetime": date_val,
"temp_c": temp_c,
"wind_kmh": wind_kmh,
"gust_kmh": gust_kmh,
"pressure_hpa": pressure,
"precip_mm": precip,
"rain_prob": rain_prob,
"cloud_pct": clouds,
"sun_min": sun_min,
"wind_dir": wind_dir,
"icon": weather_icon(clouds, precip, rain_prob, temp_c),
})
return forecast, station_info
except Exception:
traceback.print_exc()
return [], {}
def wind_direction_name(degrees):
if degrees is None or _isnan(degrees):
return ""
dirs = ["N", "NNO", "NO", "ONO", "O", "OSO", "SO", "SSO",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
idx = round(float(degrees) / 22.5) % 16
return dirs[idx]
def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c):
# Schnee
if temp_c is not None and temp_c <= 0 and (precip_mm and precip_mm > 0 or rain_prob and rain_prob >= 40):
return "❄️"
# Regen (tatsächlicher Niederschlag)
if precip_mm and precip_mm > 0.2:
return "🌧️"
# Hohe Regenwahrscheinlichkeit
if rain_prob is not None and rain_prob >= 60:
return "🌦️"
if rain_prob is not None and rain_prob >= 30:
if cloud_pct is not None and cloud_pct > 50:
return "🌦️"
if cloud_pct is not None:
if cloud_pct > 80:
return "☁️"
if cloud_pct > 35:
return ""
return "☀️"
# ── Routen ───────────────────────────────────────────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
@app.route("/wetter", methods=["GET"])
def wetter():
ort = request.args.get("ort", "").strip()
if not ort:
return render_template("index.html", error="Bitte einen Ort eingeben.")
lat, lon, display_name = geocode_location(ort)
if lat is None:
return render_template("index.html",
error=f'Ort "{ort}" konnte nicht gefunden werden.')
forecast, mosmix_station = get_mosmix_forecast(lat, lon, hours=72)
if not forecast:
return render_template(
"index.html",
error="Keine Wetterdaten von DWD verfügbar. Bitte später erneut versuchen.",
)
station_name = mosmix_station.get("name", ort)
station_id = mosmix_station.get("station_id", "")
station_lat = float(mosmix_station.get("latitude", lat))
station_lon = float(mosmix_station.get("longitude", lon))
station_dist = round(haversine(lat, lon, station_lat, station_lon), 1)
current = forecast[0]
# Tageszusammenfassung
daily = {}
for h in forecast:
dt = h["datetime"]
day = dt.date() if hasattr(dt, "date") else str(dt)[:10]
if day not in daily:
daily[day] = {"temps": [], "precip": 0.0, "cloud": [], "wind": [], "icons": [], "rain_prob": []}
if h["temp_c"] is not None:
daily[day]["temps"].append(h["temp_c"])
daily[day]["precip"] += h.get("precip_mm") or 0
if h["cloud_pct"] is not None:
daily[day]["cloud"].append(h["cloud_pct"])
if h["wind_kmh"] is not None:
daily[day]["wind"].append(h["wind_kmh"])
if h.get("rain_prob") is not None:
daily[day]["rain_prob"].append(h["rain_prob"])
daily[day]["icons"].append(h["icon"])
daily_summary = []
for day, d in daily.items():
daily_summary.append({
"date": day,
"temp_min": min(d["temps"]) if d["temps"] else None,
"temp_max": max(d["temps"]) if d["temps"] else None,
"precip": round(d["precip"], 1),
"rain_prob": max(d["rain_prob"]) if d["rain_prob"] else None,
"cloud": round(sum(d["cloud"]) / len(d["cloud"])) if d["cloud"] else None,
"wind_max": max(d["wind"]) if d["wind"] else None,
"icon": max(set(d["icons"]), key=d["icons"].count),
})
# Chart-Daten (erste 48 h)
chart_labels, chart_temps, chart_precip = [], [], []
for h in forecast[:48]:
dt = h["datetime"]
label = (dt.strftime("%d.%m %H:%M") if hasattr(dt, "strftime")
else str(dt)[5:16])
chart_labels.append(label)
chart_temps.append(h["temp_c"])
chart_precip.append(h.get("precip_mm") or 0)
return render_template(
"weather.html",
ort=ort,
display_name=display_name,
lat=lat,
lon=lon,
station_name=station_name,
station_id=station_id,
station_dist=station_dist,
current=current,
forecast=forecast[:48],
daily=daily_summary,
chart_labels=chart_labels,
chart_temps=chart_temps,
chart_precip=chart_precip,
wind_dir_name=wind_direction_name,
)
@app.route("/api/suggest")
def suggest():
q = request.args.get("q", "").strip()
if len(q) < 2:
return jsonify([])
geolocator = Nominatim(user_agent="dwd-wetter-app/1.0")
try:
results = geolocator.geocode(
q, exactly_one=False, limit=5, language="de",
addressdetails=True, timeout=5,
)
return jsonify(
[{"name": r.address, "lat": r.latitude, "lon": r.longitude}
for r in results]
if results else []
)
except Exception:
return jsonify([])
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)