From 919908e4c29f19056ae45361bb493e018f1d7111 Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 24 Apr 2026 11:40:50 +0200 Subject: [PATCH] modified: app.py modified: static/css/style.css --- app.py | 35 ++++++++++++++++++++++++++++++++--- static/css/style.css | 2 ++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 444210e..ef9a816 100644 --- a/app.py +++ b/app.py @@ -72,6 +72,7 @@ MOSMIX_PARAMS = [ "hourly/large/sunshine_duration", "hourly/large/probability_precipitation_height_gt_0_1mm_last_1h", "hourly/large/uv_index", + "hourly/large/visibility", ] def _get_berlin(): @@ -335,14 +336,24 @@ def _parse_warning_datetime(value): # regen heavy rain # schnee snow # blitz thunderstorm (needs additional MOSMIX parameter) -# wolkig_nebel_sonne fog / mist +# 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) # nacht clear night (moon) # nacht(1) moon behind cloud # nacht(2) moon+cloud+rain # nacht(3) moon+cloud+snow -def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c, is_night=False): +def weather_icon(cloud_pct, precip_mm, rain_prob, temp_c, is_night=False, visibility_m=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): + if visibility_m < 1000: + return "nebel" + if cloud_pct is not None and cloud_pct > 60: + return "nebel_wolkig" + return "wolkig_nebel_sonne" + # 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) @@ -375,6 +386,21 @@ def pick_daily_icon(hours): """Choose the most representative icon key for a whole day.""" if not hours: return "sonne" + # Fog: majority of hours with reduced visibility and no heavy precipitation + fog_hours = [ + h for h in hours + if h.get("visibility_m") is not None + and h["visibility_m"] < 5000 + and (h.get("precip_mm") or 0) <= 0.5 + ] + if len(fog_hours) >= len(hours) // 2 and fog_hours: + avg_vis = sum(h["visibility_m"] for h in fog_hours) / len(fog_hours) + avg_cloud_fog = sum(h.get("cloud_pct") or 0 for h in fog_hours) / len(fog_hours) + if avg_vis < 1000: + return "nebel" + if avg_cloud_fog > 60: + return "nebel_wolkig" + return "wolkig_nebel_sonne" # Snow / sleet if any( h.get("temp_c") is not None and h["temp_c"] <= 2 @@ -590,6 +616,8 @@ def get_mosmix_forecast(lat, lon, hours=72): sun_min = round(float(sun)/60) if not _isnan(sun) else 0 wd = p.get("wind_direction") 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 uv_raw = p.get("uv_index") dt_local = pd.Timestamp(date_val).tz_convert(berlin).tz_localize(None) # Determine day/night for icon selection @@ -624,7 +652,8 @@ def get_mosmix_forecast(lat, lon, hours=72): "confidence": confidence_score, "confidence_label": confidence_label, "activity_score": a_score, - "icon": weather_icon(clouds, precip, rain_prob, temp_c, is_night=is_night), + "visibility_m": visibility_m, + "icon": weather_icon(clouds, precip, rain_prob, temp_c, is_night=is_night, visibility_m=visibility_m), }) result_data = (forecast, station_info) _forecast_cache[cache_key] = result_data diff --git a/static/css/style.css b/static/css/style.css index 0825577..e042297 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -441,6 +441,8 @@ main { flex: 1; } .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="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%); } /* Night icons */ .wx-icon-wrap[data-icon="nacht"] { background: linear-gradient(135deg, #080c20 0%, #141c40 100%); } .wx-icon-wrap[data-icon="nacht(1)"] { background: linear-gradient(135deg, #0c1228 0%, #1e2848 100%); }