diff --git a/app.py b/app.py index ef9a816..83343f5 100644 --- a/app.py +++ b/app.py @@ -73,6 +73,7 @@ MOSMIX_PARAMS = [ "hourly/large/probability_precipitation_height_gt_0_1mm_last_1h", "hourly/large/uv_index", "hourly/large/visibility", + "hourly/large/weather_significant", ] def _get_berlin(): @@ -335,7 +336,10 @@ def _parse_warning_datetime(value): # wolkig(1) sun+cloud+rain (showers) # regen heavy rain # 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_wolkig foggy + overcast (1000-5000 m, cloud > 60 %) # 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(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.""" # 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): @@ -354,6 +362,16 @@ def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c, is_night=False, visibi return "nebel_wolkig" 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) 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) @@ -386,6 +404,21 @@ def pick_daily_icon(hours): """Choose the most representative icon key for a whole day.""" if not hours: 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_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 vis_raw = p.get("visibility") 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") dt_local = pd.Timestamp(date_val).tz_convert(berlin).tz_localize(None) # Determine day/night for icon selection @@ -653,7 +688,8 @@ def get_mosmix_forecast(lat, lon, hours=72): "confidence_label": confidence_label, "activity_score": a_score, "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) _forecast_cache[cache_key] = result_data diff --git a/static/css/style.css b/static/css/style.css index e042297..bc5b8de 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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="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="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="nebel"] { background: linear-gradient(135deg, #7a8a96 0%, #b0c0cc 100%); } .wx-icon-wrap[data-icon="nebel_wolkig"] { background: linear-gradient(135deg, #505a64 0%, #8898a8 100%); } diff --git a/static/icons/Bildnachweis.txt b/static/icons/Bildnachweis.txt index 063be87..051c724 100644 --- a/static/icons/Bildnachweis.txt +++ b/static/icons/Bildnachweis.txt @@ -1 +1 @@ -Freepik, iconixar \ No newline at end of file +Freepik https://www.flaticon.com/de/autoren/freepik, iconixar https://www.flaticon.com/de/autoren/iconixar \ No newline at end of file diff --git a/static/icons/hagel.png b/static/icons/hagel.png new file mode 100644 index 0000000..a4012d7 Binary files /dev/null and b/static/icons/hagel.png differ diff --git a/static/icons/schneebedeckt.png b/static/icons/schneebedeckt.png new file mode 100644 index 0000000..fcc5d87 Binary files /dev/null and b/static/icons/schneebedeckt.png differ diff --git a/static/icons/schneebedecktsonne.png b/static/icons/schneebedecktsonne.png new file mode 100644 index 0000000..9547f6b Binary files /dev/null and b/static/icons/schneebedecktsonne.png differ diff --git a/static/icons/sturm.png b/static/icons/sturm.png new file mode 100644 index 0000000..844fccf Binary files /dev/null and b/static/icons/sturm.png differ diff --git a/static/icons/sturmundhagel.png b/static/icons/sturmundhagel.png new file mode 100644 index 0000000..c56deba Binary files /dev/null and b/static/icons/sturmundhagel.png differ