modified: app.py

modified:   requirements.txt
	modified:   static/css/style.css
	modified:   templates/base.html
	modified:   templates/weather.html
This commit is contained in:
simon
2026-04-27 10:16:45 +02:00
parent 31d58800f0
commit e793804cea
5 changed files with 75 additions and 17 deletions

56
app.py
View File

@@ -84,6 +84,27 @@ def _get_berlin():
import pytz import pytz
return pytz.timezone("Europe/Berlin") 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): def _normalize_text(value):
if not value: if not value:
return "" return ""
@@ -510,26 +531,27 @@ def feels_like(temp_c, wind_kmh, cloud_pct):
def get_sun_times(lat, lon, date=None): def get_sun_times(lat, lon, date=None):
try: 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() 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"), return (s["sunrise"].strftime("%H:%M"), s["sunset"].strftime("%H:%M"),
s["dawn"].strftime("%H:%M"), s["dusk"].strftime("%H:%M")) s["dawn"].strftime("%H:%M"), s["dusk"].strftime("%H:%M"))
except Exception: except Exception:
return None, None, None, None return None, None, None, None
def _sunrise_sunset(lat, lon, d): 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.""" Results are cached in _sun_cache to avoid recomputing for every forecast hour."""
key = (round(lat, 1), round(lon, 1), d) key = (round(lat, 1), round(lon, 1), d)
if key in _sun_cache: if key in _sun_cache:
return _sun_cache[key] return _sun_cache[key]
try: try:
berlin = _get_berlin() loc_tz, tz_name = _get_location_tz(lat, lon)
loc = LocationInfo(latitude=lat, longitude=lon, timezone="Europe/Berlin") loc = LocationInfo(latitude=lat, longitude=lon, timezone=tz_name)
s = astral_sun(loc.observer, date=d, tzinfo=berlin) s = astral_sun(loc.observer, date=d, tzinfo=loc_tz)
sr = pd.Timestamp(s["sunrise"]).tz_convert(berlin).tz_localize(None) sr = pd.Timestamp(s["sunrise"]).tz_convert(loc_tz).tz_localize(None)
ss = pd.Timestamp(s["sunset"]).tz_convert(berlin).tz_localize(None) ss = pd.Timestamp(s["sunset"]).tz_convert(loc_tz).tz_localize(None)
result = (sr, ss) result = (sr, ss)
except Exception: except Exception:
result = (None, None) result = (None, None)
@@ -616,12 +638,13 @@ def wind_direction_name(degrees):
idx = round(float(degrees)/22.5) % 16 idx = round(float(degrees)/22.5) % 16
return dirs[idx] 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) cache_key = (round(lat,2), round(lon,2), hours)
if cache_key in _forecast_cache: if cache_key in _forecast_cache:
return _forecast_cache[cache_key] return _forecast_cache[cache_key]
try: try:
berlin = _get_berlin() if loc_tz is None:
loc_tz, _ = _get_location_tz(lat, lon)
req = DwdMosmixRequest(parameters=MOSMIX_PARAMS) req = DwdMosmixRequest(parameters=MOSMIX_PARAMS)
nearest = req.filter_by_rank(latlon=(lat, lon), rank=1) nearest = req.filter_by_rank(latlon=(lat, lon), rank=1)
result = nearest.values.all() result = nearest.values.all()
@@ -671,7 +694,7 @@ def get_mosmix_forecast(lat, lon, hours=72):
ww_raw = p.get("weather_significant") ww_raw = p.get("weather_significant")
weather_code = int(float(ww_raw)) if not _isnan(ww_raw) else None 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(loc_tz).tz_localize(None)
# Determine day/night for icon selection # Determine day/night for icon selection
_sr, _ss = _sunrise_sunset(lat, lon, dt_local.date()) _sr, _ss = _sunrise_sunset(lat, lon, dt_local.date())
is_night = bool( is_night = bool(
@@ -821,7 +844,8 @@ def wetter():
lat, lon, display_name, state_hint, location_names = geocode_location(ort) lat, lon, display_name, state_hint, location_names = geocode_location(ort)
if lat is None: if lat is None:
return render_template("index.html", error=f'Ort "{ort}" konnte nicht gefunden werden.') 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: if not forecast:
return render_template("index.html", error="Keine Wetterdaten verfügbar. Bitte später erneut versuchen.") return render_template("index.html", error="Keine Wetterdaten verfügbar. Bitte später erneut versuchen.")
station_name = mosmix_station.get("name", ort) station_name = mosmix_station.get("name", ort)
@@ -829,15 +853,14 @@ def wetter():
station_lat = float(mosmix_station.get("latitude", lat)) station_lat = float(mosmix_station.get("latitude", lat))
station_lon = float(mosmix_station.get("longitude", lon)) station_lon = float(mosmix_station.get("longitude", lon))
station_dist = round(haversine(lat, lon, station_lat, station_lon), 1) station_dist = round(haversine(lat, lon, station_lat, station_lon), 1)
berlin = _get_berlin() now_local_dt = _dt.datetime.now(loc_tz)
now_local_dt = _dt.datetime.now(berlin)
now_local = now_local_dt.strftime("%H:%M") 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 current_idx = 0
for i, h in enumerate(forecast): for i, h in enumerate(forecast):
dt = h["datetime"] dt = h["datetime"]
dt_naive = dt.replace(tzinfo=None) if hasattr(dt,"tzinfo") and dt.tzinfo is not None else dt 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 current_idx = i
break break
current = forecast[current_idx] current = forecast[current_idx]
@@ -889,6 +912,7 @@ def wetter():
ort=ort, display_name=display_name, lat=lat, lon=lon, ort=ort, display_name=display_name, lat=lat, lon=lon,
station_name=station_name, station_id=station_id, station_dist=station_dist, station_name=station_name, station_id=station_id, station_dist=station_dist,
current=current, now_local=now_local, current=current, now_local=now_local,
location_tz=location_tz,
pressure_delta=pressure_delta, pressure_trend=pressure_trend, pressure_delta=pressure_delta, pressure_trend=pressure_trend,
temp_delta_6h=temp_delta_6h, temp_trend_6h=temp_trend_6h, temp_delta_6h=temp_delta_6h, temp_trend_6h=temp_trend_6h,
best_window=best_window, best_window=best_window,

View File

@@ -7,3 +7,4 @@ requests>=2.31.0
gunicorn>=21.2.0 gunicorn>=21.2.0
cachetools>=5.3.0 cachetools>=5.3.0
astral>=3.2 astral>=3.2
timezonefinder>=6.2.0

View File

@@ -54,6 +54,13 @@ main { flex: 1; }
.nav-logo span { color: var(--orange); } .nav-logo span { color: var(--orange); }
.nav-search { flex: 1; max-width: 420px; margin-left: auto; } .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-wrap { position: relative; display: flex; align-items: center; }
.nav-search-icon { .nav-search-icon {
position: absolute; left: 10px; width: 16px; height: 16px; position: absolute; left: 10px; width: 16px; height: 16px;

View File

@@ -19,6 +19,7 @@
<ul class="ac-list" id="nav-ac"></ul> <ul class="ac-list" id="nav-ac"></ul>
</div> </div>
</form> </form>
<span class="nav-clock" id="nav-clock"></span>
</nav> </nav>
<main>{% block content %}{% endblock %}</main> <main>{% block content %}{% endblock %}</main>
@@ -65,6 +66,30 @@ function setupAC(input, list) {
} }
setupAC(document.getElementById("nav-ort"), document.getElementById("nav-ac")); setupAC(document.getElementById("nav-ort"), document.getElementById("nav-ac"));
window.setupAC = setupAC; 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);
})();
</script> </script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>

View File

@@ -2,6 +2,7 @@
{% block title %}{{ display_name.split(',')[0] }} Skywatcher{% endblock %} {% block title %}{{ display_name.split(',')[0] }} Skywatcher{% endblock %}
{% block head %} {% block head %}
<meta name="location-tz" content="{{ location_tz }}">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
{% endblock %} {% endblock %}
@@ -22,7 +23,7 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
{{ display_name.split(',')[0] }} {{ display_name.split(',')[0] }}
<span class="hero-meta-sub">{{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }}</span> <span class="hero-meta-sub">{{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }}</span>
<span class="hero-meta-time">{{ now_local }} Uhr</span> <span class="hero-meta-time" id="hero-time">{{ now_local }} Uhr</span>
</div> </div>
<div class="hero-main"> <div class="hero-main">