modified: app.py
modified: static/css/style.css modified: static/icons/Bildnachweis.txt deleted: static/icons/mond.png new file: static/icons/nacht(1).png new file: static/icons/nacht(2).png new file: static/icons/nacht(3).png new file: static/icons/nacht.png new file: static/icons/nebel.png new file: static/icons/nebel_wolkig.png new file: static/icons/wolkig_nebel_sonne.png modified: templates/weather.html
This commit is contained in:
80
app.py
80
app.py
@@ -33,6 +33,7 @@ except ImportError:
|
||||
_forecast_cache = TTLCache(maxsize=64, ttl=2700)
|
||||
_warn_cache = TTLCache(maxsize=32, ttl=900)
|
||||
_suggest_cache = TTLCache(maxsize=256, ttl=900)
|
||||
_sun_cache: dict = {} # (lat_r, lon_r, date) -> (naive_sunrise_ts, naive_sunset_ts)
|
||||
|
||||
# API protection: simple in-memory rate limiting by client IP.
|
||||
_suggest_rate_lock = Lock()
|
||||
@@ -325,15 +326,50 @@ def _parse_warning_datetime(value):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ── Icon key → static/icons/{key}.png mapping ────────────────────────────────
|
||||
# sonne = clear sky
|
||||
# wolkig(2) = sun behind one cloud (partly cloudy)
|
||||
# wolke = single cloud (mostly cloudy)
|
||||
# wolkig = two clouds (overcast)
|
||||
# wolkig(1) = sun + cloud + rain drops (showers)
|
||||
# regen = heavy cloud + rain (rain)
|
||||
# schnee = cloud + snowflakes
|
||||
# blitz = two clouds + lightning (thunderstorm – not yet used, needs data)
|
||||
# ── Icon key → static/icons/{key}.png ──────────────────────────────────────
|
||||
# sonne clear day
|
||||
# wolkig(2) sun + one cloud (partly cloudy)
|
||||
# wolke single cloud (mostly cloudy)
|
||||
# wolkig two clouds (overcast)
|
||||
# wolkig(1) sun+cloud+rain (showers)
|
||||
# regen heavy rain
|
||||
# schnee snow
|
||||
# blitz thunderstorm (needs additional MOSMIX parameter)
|
||||
# wolkig_nebel_sonne fog / mist
|
||||
# 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):
|
||||
"""Return the icon key for static/icons/{key}.png."""
|
||||
# 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)
|
||||
):
|
||||
return "nacht(3)" if is_night else "schnee"
|
||||
|
||||
# ── Night icons ───────────────────────────────────────────────────
|
||||
if is_night:
|
||||
if (precip_mm and precip_mm > 0.1) or (rain_prob is not None and rain_prob >= 50):
|
||||
return "nacht(2)"
|
||||
if rain_prob is not None and rain_prob >= 30 and cloud_pct is not None and cloud_pct > 50:
|
||||
return "nacht(2)"
|
||||
if cloud_pct is not None:
|
||||
if cloud_pct > 80: return "wolkig" # too cloudy to see moon
|
||||
if cloud_pct > 30: return "nacht(1)"
|
||||
return "nacht"
|
||||
|
||||
# ── Day icons ─────────────────────────────────────────────────────
|
||||
if (precip_mm and precip_mm > 0.1) or (rain_prob is not None and rain_prob >= 50):
|
||||
return "wolkig(1)" if (cloud_pct is None or cloud_pct < 75) else "regen"
|
||||
if rain_prob is not None and rain_prob >= 30 and cloud_pct is not None and cloud_pct > 50:
|
||||
return "wolkig(1)"
|
||||
if cloud_pct is not None:
|
||||
if cloud_pct > 80: return "wolkig"
|
||||
if cloud_pct > 50: return "wolke"
|
||||
if cloud_pct > 20: return "wolkig(2)"
|
||||
return "sonne"
|
||||
|
||||
def pick_daily_icon(hours):
|
||||
"""Choose the most representative icon key for a whole day."""
|
||||
@@ -406,6 +442,24 @@ def get_sun_times(lat, lon, date=None):
|
||||
except Exception:
|
||||
return None, None, None, None
|
||||
|
||||
def _sunrise_sunset(lat, lon, d):
|
||||
"""Return (naive_sunrise, naive_sunset) in Berlin local time for date d.
|
||||
Results are cached in _sun_cache to avoid recomputing for every forecast hour."""
|
||||
key = (round(lat, 1), round(lon, 1), d)
|
||||
if key in _sun_cache:
|
||||
return _sun_cache[key]
|
||||
try:
|
||||
berlin = _get_berlin()
|
||||
loc = LocationInfo(latitude=lat, longitude=lon, timezone="Europe/Berlin")
|
||||
s = astral_sun(loc.observer, date=d, tzinfo=berlin)
|
||||
sr = pd.Timestamp(s["sunrise"]).tz_convert(berlin).tz_localize(None)
|
||||
ss = pd.Timestamp(s["sunset"]).tz_convert(berlin).tz_localize(None)
|
||||
result = (sr, ss)
|
||||
except Exception:
|
||||
result = (None, None)
|
||||
_sun_cache[key] = result
|
||||
return result
|
||||
|
||||
def get_dwd_warnings(lat, lon, state_hint=None, location_names=None):
|
||||
"""Fetch DWD warnings and filter by regionName matching the user's municipality/county.
|
||||
|
||||
@@ -558,6 +612,12 @@ def get_mosmix_forecast(lat, lon, hours=72):
|
||||
wind_dir = float(wd) if not _isnan(wd) 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
|
||||
_sr, _ss = _sunrise_sunset(lat, lon, dt_local.date())
|
||||
is_night = bool(
|
||||
_sr is not None and _ss is not None
|
||||
and (pd.Timestamp(dt_local) < _sr or pd.Timestamp(dt_local) > _ss)
|
||||
)
|
||||
if not _isnan(uv_raw):
|
||||
uv = round(float(uv_raw), 1)
|
||||
else:
|
||||
@@ -584,7 +644,7 @@ 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),
|
||||
"icon": weather_icon(clouds, precip, rain_prob, temp_c, is_night=is_night),
|
||||
})
|
||||
result_data = (forecast, station_info)
|
||||
_forecast_cache[cache_key] = result_data
|
||||
|
||||
Reference in New Issue
Block a user