diff --git a/app.py b/app.py index 456a2c5..5cdf01b 100644 --- a/app.py +++ b/app.py @@ -84,6 +84,27 @@ def _get_berlin(): import pytz return pytz.timezone("Europe/Berlin") +_TF = None # TimezoneFinder singleton + +def _get_location_tz(lat, lon): + """Return (tzinfo, tz_name) for the given coordinates using timezonefinder.""" + global _TF + try: + if _TF is None: + from timezonefinder import TimezoneFinder + _TF = TimezoneFinder() + tz_name = _TF.timezone_at(lat=lat, lng=lon) + if tz_name: + try: + import zoneinfo + return zoneinfo.ZoneInfo(tz_name), tz_name + except ImportError: + import pytz + return pytz.timezone(tz_name), tz_name + except Exception: + pass + return _get_berlin(), "Europe/Berlin" + def _normalize_text(value): if not value: return "" @@ -510,26 +531,27 @@ def feels_like(temp_c, wind_kmh, cloud_pct): def get_sun_times(lat, lon, date=None): try: - loc = LocationInfo(latitude=lat, longitude=lon, timezone="Europe/Berlin") + loc_tz, tz_name = _get_location_tz(lat, lon) + loc = LocationInfo(latitude=lat, longitude=lon, timezone=tz_name) d = date or _dt.date.today() - s = astral_sun(loc.observer, date=d, tzinfo=_get_berlin()) + s = astral_sun(loc.observer, date=d, tzinfo=loc_tz) return (s["sunrise"].strftime("%H:%M"), s["sunset"].strftime("%H:%M"), s["dawn"].strftime("%H:%M"), s["dusk"].strftime("%H:%M")) 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. + """Return (naive_sunrise, naive_sunset) in location 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) + loc_tz, tz_name = _get_location_tz(lat, lon) + loc = LocationInfo(latitude=lat, longitude=lon, timezone=tz_name) + s = astral_sun(loc.observer, date=d, tzinfo=loc_tz) + sr = pd.Timestamp(s["sunrise"]).tz_convert(loc_tz).tz_localize(None) + ss = pd.Timestamp(s["sunset"]).tz_convert(loc_tz).tz_localize(None) result = (sr, ss) except Exception: result = (None, None) @@ -616,12 +638,13 @@ def wind_direction_name(degrees): idx = round(float(degrees)/22.5) % 16 return dirs[idx] -def get_mosmix_forecast(lat, lon, hours=72): +def get_mosmix_forecast(lat, lon, hours=72, loc_tz=None): cache_key = (round(lat,2), round(lon,2), hours) if cache_key in _forecast_cache: return _forecast_cache[cache_key] try: - berlin = _get_berlin() + if loc_tz is None: + loc_tz, _ = _get_location_tz(lat, lon) req = DwdMosmixRequest(parameters=MOSMIX_PARAMS) nearest = req.filter_by_rank(latlon=(lat, lon), rank=1) result = nearest.values.all() @@ -671,7 +694,7 @@ def get_mosmix_forecast(lat, lon, hours=72): 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) + dt_local = pd.Timestamp(date_val).tz_convert(loc_tz).tz_localize(None) # Determine day/night for icon selection _sr, _ss = _sunrise_sunset(lat, lon, dt_local.date()) is_night = bool( @@ -821,7 +844,8 @@ def wetter(): lat, lon, display_name, state_hint, location_names = 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=240) + loc_tz, location_tz = _get_location_tz(lat, lon) + forecast, mosmix_station = get_mosmix_forecast(lat, lon, hours=240, loc_tz=loc_tz) if not forecast: return render_template("index.html", error="Keine Wetterdaten verfügbar. Bitte später erneut versuchen.") station_name = mosmix_station.get("name", ort) @@ -829,15 +853,14 @@ def wetter(): 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) - berlin = _get_berlin() - now_local_dt = _dt.datetime.now(berlin) + now_local_dt = _dt.datetime.now(loc_tz) now_local = now_local_dt.strftime("%H:%M") - now_berlin_naive = now_local_dt.replace(minute=0, second=0, microsecond=0, tzinfo=None) + now_loc_naive = now_local_dt.replace(minute=0, second=0, microsecond=0, tzinfo=None) current_idx = 0 for i, h in enumerate(forecast): dt = h["datetime"] dt_naive = dt.replace(tzinfo=None) if hasattr(dt,"tzinfo") and dt.tzinfo is not None else dt - if dt_naive >= now_berlin_naive: + if dt_naive >= now_loc_naive: current_idx = i break current = forecast[current_idx] @@ -889,6 +912,7 @@ def wetter(): ort=ort, display_name=display_name, lat=lat, lon=lon, station_name=station_name, station_id=station_id, station_dist=station_dist, current=current, now_local=now_local, + location_tz=location_tz, pressure_delta=pressure_delta, pressure_trend=pressure_trend, temp_delta_6h=temp_delta_6h, temp_trend_6h=temp_trend_6h, best_window=best_window, diff --git a/requirements.txt b/requirements.txt index d707ac8..1916c81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ requests>=2.31.0 gunicorn>=21.2.0 cachetools>=5.3.0 astral>=3.2 +timezonefinder>=6.2.0 diff --git a/static/css/style.css b/static/css/style.css index 1e5aaee..b23a852 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -54,6 +54,13 @@ main { flex: 1; } .nav-logo span { color: var(--orange); } .nav-search { flex: 1; max-width: 420px; margin-left: auto; } +.nav-clock { + flex-shrink: 0; + font-size: 0.85rem; font-weight: 500; + color: var(--muted); + letter-spacing: 0.02em; + white-space: nowrap; +} .nav-search-wrap { position: relative; display: flex; align-items: center; } .nav-search-icon { position: absolute; left: 10px; width: 16px; height: 16px; diff --git a/templates/base.html b/templates/base.html index 70a2435..ed3758d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -19,6 +19,7 @@ +
{% block content %}{% endblock %}
@@ -65,6 +66,30 @@ function setupAC(input, list) { } setupAC(document.getElementById("nav-ort"), document.getElementById("nav-ac")); window.setupAC = setupAC; + +// Live clock — shows location's local time when a timezone meta tag is present +(function () { + const tzMeta = document.querySelector('meta[name="location-tz"]'); + const tz = tzMeta ? tzMeta.getAttribute("content") : null; + const navClock = document.getElementById("nav-clock"); + const heroTime = document.getElementById("hero-time"); + + function tick() { + const now = new Date(); + const opts = { hour: "2-digit", minute: "2-digit", hour12: false }; + let timeStr; + try { + timeStr = new Intl.DateTimeFormat("de-DE", tz ? { ...opts, timeZone: tz } : opts).format(now); + } catch (e) { + timeStr = new Intl.DateTimeFormat("de-DE", opts).format(now); + } + if (navClock) navClock.textContent = timeStr + " Uhr"; + if (heroTime) heroTime.textContent = timeStr + " Uhr"; + } + + tick(); + setInterval(tick, 10000); +})(); {% block scripts %}{% endblock %} diff --git a/templates/weather.html b/templates/weather.html index 73dd13c..cccc0c4 100644 --- a/templates/weather.html +++ b/templates/weather.html @@ -2,6 +2,7 @@ {% block title %}{{ display_name.split(',')[0] }} – Skywatcher{% endblock %} {% block head %} + {% endblock %} @@ -22,7 +23,7 @@ {{ display_name.split(',')[0] }} {{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }} - {{ now_local }} Uhr + {{ now_local }} Uhr