modified: app.py
modified: requirements.txt modified: static/css/style.css modified: templates/base.html modified: templates/weather.html
This commit is contained in:
56
app.py
56
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,
|
||||
|
||||
@@ -7,3 +7,4 @@ requests>=2.31.0
|
||||
gunicorn>=21.2.0
|
||||
cachetools>=5.3.0
|
||||
astral>=3.2
|
||||
timezonefinder>=6.2.0
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<ul class="ac-list" id="nav-ac"></ul>
|
||||
</div>
|
||||
</form>
|
||||
<span class="nav-clock" id="nav-clock"></span>
|
||||
</nav>
|
||||
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
@@ -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);
|
||||
})();
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% block title %}{{ display_name.split(',')[0] }} – Skywatcher{% endblock %}
|
||||
|
||||
{% 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>
|
||||
{% 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>
|
||||
{{ 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-time">{{ now_local }} Uhr</span>
|
||||
<span class="hero-meta-time" id="hero-time">{{ now_local }} Uhr</span>
|
||||
</div>
|
||||
|
||||
<div class="hero-main">
|
||||
|
||||
Reference in New Issue
Block a user