modified: app.py

modified:   static/css/style.css
	modified:   templates/weather.html
This commit is contained in:
simon
2026-04-22 14:33:00 +02:00
parent 7b3b66c6fc
commit 3294ccf45a
3 changed files with 85 additions and 4 deletions

27
app.py
View File

@@ -193,6 +193,22 @@ def _round_temp(k):
def _clamp(value, min_value, max_value):
return max(min_value, min(max_value, value))
def _estimate_uv(dt_local, lat, cloud_pct):
"""Rough UV index estimate for Central Europe when the MOSMIX station doesn't
provide the UVI parameter. Uses time-of-day, season, latitude, and cloud cover."""
hour = dt_local.hour + dt_local.minute / 60.0
if hour < 5.5 or hour > 20.5:
return 0.0
noon = 13.0 # approximate solar noon in Germany (CET)
hour_factor = max(0.0, math.cos(math.pi * (hour - noon) / 14.0))
# Typical clear-sky peak UV at solar noon for ~51°N, JanDec
monthly_peak = [1.0, 1.8, 3.5, 5.0, 6.5, 7.5, 7.2, 6.2, 4.5, 2.5, 1.2, 0.8]
seasonal = monthly_peak[dt_local.month - 1]
lat_factor = max(0.5, 1.0 - (float(lat) - 51.0) * 0.015)
cloud_factor = 1.0 - (cloud_pct or 0) / 100.0 * 0.75
uv = seasonal * hour_factor * lat_factor * cloud_factor
return round(max(0.0, uv), 1)
def uv_risk_info(uv_index):
if uv_index is None:
return "", "na"
@@ -286,9 +302,9 @@ def temp_trend_info(forecast, step_hours=6):
return None, None
delta = round(t1 - t0, 1)
if delta >= 1.0:
return delta, "waermer"
return delta, "wärmer"
if delta <= -1.0:
return delta, "kaelter"
return delta, "kälter"
return delta, "konstant"
def _parse_warning_datetime(value):
@@ -489,12 +505,15 @@ def get_mosmix_forecast(lat, lon, hours=72):
wd = p.get("wind_direction")
wind_dir = float(wd) if not _isnan(wd) else None
uv_raw = p.get("uv_index")
uv = round(float(uv_raw),1) if not _isnan(uv_raw) else None
dt_local = pd.Timestamp(date_val).tz_convert(berlin).tz_localize(None)
if not _isnan(uv_raw):
uv = round(float(uv_raw), 1)
else:
uv = _estimate_uv(dt_local, lat, clouds)
uv_label, uv_level = uv_risk_info(uv)
feels = feels_like(temp_c, wind_kmh, clouds)
confidence_score, confidence_label = hour_confidence_score(temp_c, precip, rain_prob, wind_kmh, gust_kmh, clouds)
a_score = activity_score(temp_c, precip, rain_prob, wind_kmh, gust_kmh, uv)
dt_local = pd.Timestamp(date_val).tz_convert(berlin).tz_localize(None)
forecast.append({
"datetime": dt_local,
"temp_c": temp_c,

View File

@@ -511,6 +511,7 @@ main { flex: 1; }
.hero { padding: 2rem 1.25rem 1.25rem; }
.section { padding: 2rem 1.25rem 0; }
.drow { grid-template-columns: 64px 1fr 120px; padding: 0.75rem 0.9rem; gap: 0.6rem; }
.daily-header { grid-template-columns: 64px 1fr 120px; padding: 0.45rem 0.9rem; gap: 0.6rem; }
.hero-metrics { border-radius: 10px; }
.hero-metric { padding: 0.7rem 0.9rem; min-width: 80px; }
.hm-val { font-size: 0.95rem; }
@@ -580,6 +581,45 @@ main { flex: 1; }
border-radius: 99px; padding: 0.1rem 0.45rem;
margin-left: 0.4rem;
}
/* Hourly legend */
.hourly-legend {
display: flex; flex-wrap: wrap; align-items: center;
gap: 0.4rem; margin-bottom: 0.75rem;
}
.hl-label {
font-size: 0.67rem; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.8px;
font-weight: 600; margin-right: 0.2rem;
}
.hl-sep { color: var(--muted); font-size: 0.75rem; }
.hl-badge {
padding: 0.1rem 0.45rem; border-radius: 99px;
font-size: 0.65rem; font-weight: 600;
}
.hl-conf--hoch { color: #34d399; background: rgba(52,211,153,0.14); }
.hl-conf--mittel { color: #fbbf24; background: rgba(251,191,36,0.14); }
.hl-conf--niedrig { color: #f87171; background: rgba(248,113,113,0.14); }
.hl-act { color: #c7d2fe; background: rgba(129,140,248,0.15); }
.hl-uv { color: #a78bfa; background: rgba(167,139,250,0.12); }
/* Daily list header */
.daily-header {
display: grid;
grid-template-columns: 80px 1fr 160px;
padding: 0.45rem 1.2rem;
gap: 1rem;
font-size: 0.67rem; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.8px;
color: var(--muted);
border: 1px solid rgba(255,255,255,0.07);
border-bottom: none;
border-radius: var(--r) var(--r) 0 0;
background: rgba(255,255,255,0.02);
}
.daily-header + .daily-list {
border-radius: 0 0 var(--r) var(--r);
}
.recent-label {
font-size: 0.68rem; text-transform: uppercase;
letter-spacing: 1px; color: var(--muted);

View File

@@ -43,11 +43,18 @@
</div>
<div class="hero-metrics">
{% if current.uv_index is not none %}
{% set _uv_l, _uv_lv = uv_risk_info(current.uv_index) %}
{% set uv_curr_str = current.uv_index|string + " " + _uv_l %}
{% else %}
{% set uv_curr_str = "" %}
{% endif %}
{% set items = [
("Gefühlt wie", (current.feels_like|string + " °C") if current.feels_like is not none else ""),
("Böen", (current.gust_kmh|string + " km/h") if current.gust_kmh is not none else ""),
("Niederschlag", (current.precip_mm|string + " mm") if (current.precip_mm is not none and current.precip_mm > 0) else ((current.rain_prob|string + " %") if (current.rain_prob is not none and current.rain_prob > 0) else "0 mm")),
("Sonne", (current.sun_min|string + " min/h") if (current.sun_min is not none and current.sun_min > 0) else ""),
("UV-Index", uv_curr_str),
] %}
{% for label, val in items %}
<div class="hero-metric">
@@ -138,6 +145,16 @@
<!-- STÜNDLICH -->
<section class="section">
<h2 class="section-title">Stundenweise</h2>
<div class="hourly-legend">
<span class="hl-label">Legende:</span>
<span class="hl-badge hl-conf hl-conf--hoch">Konfidenz hoch</span>
<span class="hl-badge hl-conf hl-conf--mittel">mittel</span>
<span class="hl-badge hl-conf hl-conf--niedrig">niedrig</span>
<span class="hl-sep">·</span>
<span class="hl-badge hl-act">Aktivität 0100</span>
<span class="hl-sep">·</span>
<span class="hl-badge hl-uv">UV-Index</span>
</div>
<div class="hourly-strip-wrap">
<div class="hourly-strip">
{% for h in forecast %}
@@ -190,6 +207,11 @@
<!-- TAGESÜBERSICHT -->
<section class="section">
<h2 class="section-title">Tagesübersicht</h2>
<div class="daily-header">
<span>Tag</span>
<span>Temperaturbereich</span>
<span>Min &nbsp;·&nbsp; Max &nbsp;·&nbsp; Nieder. &nbsp;·&nbsp; UV</span>
</div>
<div class="daily-list">
{# calc global min/max for bar scaling do it in a loop to stay Jinja2-safe #}
{% set ns = namespace(g_min=99, g_max=-99) %}