modified: app.py

modified:   static/css/style.css
	modified:   static/icons/Bildnachweis.txt
	new file:   static/icons/hagel.png
	new file:   static/icons/schneebedeckt.png
	new file:   static/icons/schneebedecktsonne.png
	new file:   static/icons/sturm.png
	new file:   static/icons/sturmundhagel.png
This commit is contained in:
simon
2026-04-24 11:49:50 +02:00
parent 919908e4c2
commit b89a1215cd
8 changed files with 44 additions and 5 deletions

42
app.py
View File

@@ -73,6 +73,7 @@ MOSMIX_PARAMS = [
"hourly/large/probability_precipitation_height_gt_0_1mm_last_1h", "hourly/large/probability_precipitation_height_gt_0_1mm_last_1h",
"hourly/large/uv_index", "hourly/large/uv_index",
"hourly/large/visibility", "hourly/large/visibility",
"hourly/large/weather_significant",
] ]
def _get_berlin(): def _get_berlin():
@@ -335,7 +336,10 @@ def _parse_warning_datetime(value):
# wolkig(1) sun+cloud+rain (showers) # wolkig(1) sun+cloud+rain (showers)
# regen heavy rain # regen heavy rain
# schnee snow # schnee snow
# blitz thunderstorm (needs additional MOSMIX parameter) # blitz dry thunderstorm (WW 17, 91-92)
# sturm thunderstorm with rain (WW 95, 97-98)
# hagel hail (WW 27, 89-90, 93-94, 96, 99)
# schneebedeckt hail + snow (hagel WW + temp ≤ 2°C)
# nebel dense fog (visibility < 1000 m) # nebel dense fog (visibility < 1000 m)
# nebel_wolkig foggy + overcast (1000-5000 m, cloud > 60 %) # nebel_wolkig foggy + overcast (1000-5000 m, cloud > 60 %)
# wolkig_nebel_sonne patchy fog / haze (1000-5000 m, less cloud) # wolkig_nebel_sonne patchy fog / haze (1000-5000 m, less cloud)
@@ -344,7 +348,11 @@ def _parse_warning_datetime(value):
# nacht(2) moon+cloud+rain # nacht(2) moon+cloud+rain
# nacht(3) moon+cloud+snow # nacht(3) moon+cloud+snow
def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c, is_night=False, visibility_m=None): _WW_HAIL = {27, 89, 90, 93, 94, 96, 99} # hail shower codes
_WW_THUNDER_RAIN = {91, 92, 95, 97, 98} # thunderstorm + precipitation
_WW_THUNDER_DRY = {17} # dry thunderstorm
def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c, is_night=False, visibility_m=None, weather_code=None):
"""Return the icon key for static/icons/{key}.png.""" """Return the icon key for static/icons/{key}.png."""
# Fog (takes priority; fog unlikely when precipitating heavily) # Fog (takes priority; fog unlikely when precipitating heavily)
if visibility_m is not None and visibility_m < 5000 and not (precip_mm and precip_mm > 0.5): if visibility_m is not None and visibility_m < 5000 and not (precip_mm and precip_mm > 0.5):
@@ -354,6 +362,16 @@ def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c, is_night=False, visibi
return "nebel_wolkig" return "nebel_wolkig"
return "wolkig_nebel_sonne" return "wolkig_nebel_sonne"
# ── WW-code based icons (thunderstorm / hail) ─────────────────────
if weather_code is not None:
ww = int(weather_code)
if ww in _WW_HAIL:
return "schneebedeckt" if (temp_c is not None and temp_c <= 2) else "hagel"
if ww in _WW_THUNDER_RAIN:
return "sturm"
if ww in _WW_THUNDER_DRY:
return "blitz"
# Snow / sleet (day and night) # Snow / sleet (day and night)
if temp_c is not None and temp_c <= 2 and ( if temp_c is not None and temp_c <= 2 and (
(precip_mm and precip_mm > 0.1) or (rain_prob and rain_prob >= 40) (precip_mm and precip_mm > 0.1) or (rain_prob and rain_prob >= 40)
@@ -386,6 +404,21 @@ def pick_daily_icon(hours):
"""Choose the most representative icon key for a whole day.""" """Choose the most representative icon key for a whole day."""
if not hours: if not hours:
return "sonne" return "sonne"
# Thunderstorm / hail: any hour with matching WW code takes priority
ww_codes = [h.get("weather_code") for h in hours if h.get("weather_code") is not None]
if ww_codes:
has_hail = any(w in _WW_HAIL for w in ww_codes)
has_t_rain = any(w in _WW_THUNDER_RAIN for w in ww_codes)
has_t_dry = any(w in _WW_THUNDER_DRY for w in ww_codes)
avg_temp = sum(h["temp_c"] for h in hours if h.get("temp_c") is not None)
n_temp = sum(1 for h in hours if h.get("temp_c") is not None)
mean_temp = avg_temp / n_temp if n_temp else 10
if has_hail:
return "schneebedeckt" if mean_temp <= 2 else "hagel"
if has_t_rain:
return "sturm"
if has_t_dry:
return "blitz"
# Fog: majority of hours with reduced visibility and no heavy precipitation # Fog: majority of hours with reduced visibility and no heavy precipitation
fog_hours = [ fog_hours = [
h for h in hours h for h in hours
@@ -618,6 +651,8 @@ def get_mosmix_forecast(lat, lon, hours=72):
wind_dir = float(wd) if not _isnan(wd) else None wind_dir = float(wd) if not _isnan(wd) else None
vis_raw = p.get("visibility") vis_raw = p.get("visibility")
visibility_m = round(float(vis_raw)) if not _isnan(vis_raw) else None visibility_m = round(float(vis_raw)) if not _isnan(vis_raw) else None
ww_raw = p.get("weather_significant")
weather_code = int(float(ww_raw)) if not _isnan(ww_raw) else None
uv_raw = p.get("uv_index") uv_raw = p.get("uv_index")
dt_local = pd.Timestamp(date_val).tz_convert(berlin).tz_localize(None) dt_local = pd.Timestamp(date_val).tz_convert(berlin).tz_localize(None)
# Determine day/night for icon selection # Determine day/night for icon selection
@@ -653,7 +688,8 @@ def get_mosmix_forecast(lat, lon, hours=72):
"confidence_label": confidence_label, "confidence_label": confidence_label,
"activity_score": a_score, "activity_score": a_score,
"visibility_m": visibility_m, "visibility_m": visibility_m,
"icon": weather_icon(clouds, precip, rain_prob, temp_c, is_night=is_night, visibility_m=visibility_m), "weather_code": weather_code,
"icon": weather_icon(clouds, precip, rain_prob, temp_c, is_night=is_night, visibility_m=visibility_m, weather_code=weather_code),
}) })
result_data = (forecast, station_info) result_data = (forecast, station_info)
_forecast_cache[cache_key] = result_data _forecast_cache[cache_key] = result_data

View File

@@ -439,7 +439,10 @@ main { flex: 1; }
.wx-icon-wrap[data-icon="wolkig(1)"] { background: linear-gradient(135deg, #1a6090 0%, #4090c0 100%); } .wx-icon-wrap[data-icon="wolkig(1)"] { background: linear-gradient(135deg, #1a6090 0%, #4090c0 100%); }
.wx-icon-wrap[data-icon="regen"] { background: linear-gradient(135deg, #0e3050 0%, #1a5080 100%); } .wx-icon-wrap[data-icon="regen"] { background: linear-gradient(135deg, #0e3050 0%, #1a5080 100%); }
.wx-icon-wrap[data-icon="schnee"] { background: linear-gradient(135deg, #3070a8 0%, #70b8e0 100%); } .wx-icon-wrap[data-icon="schnee"] { background: linear-gradient(135deg, #3070a8 0%, #70b8e0 100%); }
.wx-icon-wrap[data-icon="blitz"] { background: linear-gradient(135deg, #1e1040 0%, #5030a0 100%); } .wx-icon-wrap[data-icon="blitz"] { background: linear-gradient(135deg, #1e1040 0%, #5030a0 100%); }
.wx-icon-wrap[data-icon="sturm"] { background: linear-gradient(135deg, #120e30 0%, #2a1870 60%, #0a0820 100%); }
.wx-icon-wrap[data-icon="hagel"] { background: linear-gradient(135deg, #284060 0%, #507090 100%); }
.wx-icon-wrap[data-icon="schneebedeckt"]{ background: linear-gradient(135deg, #2a3a50 0%, #80a8c8 100%); }
.wx-icon-wrap[data-icon="wolkig_nebel_sonne"] { background: linear-gradient(135deg, #706858 0%, #a09880 100%); } .wx-icon-wrap[data-icon="wolkig_nebel_sonne"] { background: linear-gradient(135deg, #706858 0%, #a09880 100%); }
.wx-icon-wrap[data-icon="nebel"] { background: linear-gradient(135deg, #7a8a96 0%, #b0c0cc 100%); } .wx-icon-wrap[data-icon="nebel"] { background: linear-gradient(135deg, #7a8a96 0%, #b0c0cc 100%); }
.wx-icon-wrap[data-icon="nebel_wolkig"] { background: linear-gradient(135deg, #505a64 0%, #8898a8 100%); } .wx-icon-wrap[data-icon="nebel_wolkig"] { background: linear-gradient(135deg, #505a64 0%, #8898a8 100%); }

View File

@@ -1 +1 @@
Freepik, iconixar Freepik https://www.flaticon.com/de/autoren/freepik, iconixar https://www.flaticon.com/de/autoren/iconixar

BIN
static/icons/hagel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
static/icons/sturm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB