modified: app.py

modified:   static/css/style.css
	modified:   templates/weather.html
This commit is contained in:
simon
2026-04-24 09:43:36 +02:00
parent def5446ea5
commit 08a8bb2e13
3 changed files with 60 additions and 32 deletions

69
app.py
View File

@@ -73,6 +73,20 @@ MOSMIX_PARAMS = [
"hourly/large/uv_index", "hourly/large/uv_index",
] ]
# ── Wetter-Icons (OpenWeatherMap CDN) ────────────────────────────────────────
_OWM = "https://openweathermap.org/img/wn/"
ICON_URLS = {
"☀️": _OWM + "01d@2x.png", # klar
"": _OWM + "02d@2x.png", # leicht bewölkt
"☁️": _OWM + "04d@2x.png", # bedeckt
"🌧️": _OWM + "10d@2x.png", # Regen
"🌦️": _OWM + "09d@2x.png", # Regenschauer
"❄️": _OWM + "13d@2x.png", # Schnee
}
def _icon_url(emoji):
return ICON_URLS.get(str(emoji), _OWM + "03d@2x.png")
def _get_berlin(): def _get_berlin():
try: try:
import zoneinfo import zoneinfo
@@ -341,41 +355,43 @@ def pick_daily_icon(hours):
return "☀️" return "☀️"
def feels_like(temp_c, wind_kmh, cloud_pct): def feels_like(temp_c, wind_kmh, cloud_pct):
"""Apparent temperature. """Apparent / perceived temperature.
* Below 10 °C with wind > 4.8 km/h: Windchill (JAG/TI formula) Uses the JAG/TI wind-chill formula blended smoothly across the full
* Above 27 °C: Heat index (Rothfusz, assumed RH 60 %) temperature range so there is no abrupt jump at the old 10 °C threshold.
* Otherwise: actual temp ± sunshine/wind adjustments Above 27 °C the Rothfusz heat index (assumed RH 60 %) is applied.
- Wind cools by up to ~3 °C even in the mild range
- Sunshine (low clouds, no wind) warms by up to +4 °C The previous cloud-based sunshine bonus has been removed: cloud cover
fluctuates strongly hour to hour, which caused the felt temperature to
jump by several degrees without any real change in conditions.
""" """
if temp_c is None: if temp_c is None:
return None return None
# ── Cold range: wind chill ────────────────────────────────────────
if temp_c <= 10 and wind_kmh is not None and wind_kmh > 4.8: # ── Hot range: heat index (Rothfusz, RH 60 %) ────────────────────
v = wind_kmh
wc = 13.12 + 0.6215*temp_c - 11.37*(v**0.16) + 0.3965*temp_c*(v**0.16)
return round(wc, 1)
# ── Hot range: heat index ─────────────────────────────────────────
if temp_c >= 27: if temp_c >= 27:
rh = 60 rh = 60
hi = (-8.78469475556 + 1.61139411*temp_c + 2.33854883889*rh hi = (-8.78469475556 + 1.61139411 * temp_c + 2.33854883889 * rh
- 0.14611605*temp_c*rh - 0.012308094*temp_c**2 - 0.14611605 * temp_c * rh - 0.012308094 * temp_c ** 2
- 0.016424828*rh**2 + 0.002211732*temp_c**2*rh - 0.016424828 * rh ** 2 + 0.002211732 * temp_c ** 2 * rh
+ 0.00072546*temp_c*rh**2 - 0.000003582*temp_c**2*rh**2) + 0.00072546 * temp_c * rh ** 2
- 0.000003582 * temp_c ** 2 * rh ** 2)
return round(hi, 1) return round(hi, 1)
# ── Mild range: sunshine bonus + light wind cooling ───────────────
# ── Wind-chill (JAG/TI), blended into mild range ──────────────────
# Full wind-chill effect at ≤ 5 °C, linearly faded to zero at ≥ 20 °C.
# This avoids the hard jump that the old ≤ 10 °C threshold caused.
wind = wind_kmh or 0
adjusted = float(temp_c) adjusted = float(temp_c)
# Sunshine bonus: clear sky (cloud_pct < 30) adds up to +4 °C if wind > 4.8:
if cloud_pct is not None and cloud_pct < 30: wc = (13.12
sunshine_bonus = (30 - cloud_pct) / 30 * 4.0 + 0.6215 * temp_c
adjusted += sunshine_bonus - 11.37 * (wind ** 0.16)
# Moderate wind cooling: >10 km/h removes up to 3 °C + 0.3965 * temp_c * (wind ** 0.16))
if wind_kmh is not None and wind_kmh > 10: blend = _clamp((20.0 - temp_c) / 15.0, 0.0, 1.0)
wind_penalty = _clamp((wind_kmh - 10) / 40 * 3.0, 0, 3.0) adjusted = temp_c * (1.0 - blend) + wc * blend
adjusted -= wind_penalty
result = round(adjusted, 1) result = round(adjusted, 1)
# Only return a value if it meaningfully differs from actual temp
return result if abs(result - temp_c) >= 0.5 else temp_c return result if abs(result - temp_c) >= 0.5 else temp_c
def get_sun_times(lat, lon, date=None): def get_sun_times(lat, lon, date=None):
@@ -747,6 +763,7 @@ def wetter():
chart_precip=chart_precip, chart_rain_prob=chart_rain_prob, chart_precip=chart_precip, chart_rain_prob=chart_rain_prob,
wind_dir_name=wind_direction_name, wind_dir_name=wind_direction_name,
uv_risk_info=uv_risk_info, uv_risk_info=uv_risk_info,
icon_url=_icon_url,
) )
@app.route("/api/suggest") @app.route("/api/suggest")

View File

@@ -299,7 +299,12 @@ main { flex: 1; }
display: flex; flex-direction: column; display: flex; flex-direction: column;
justify-content: flex-end; padding-bottom: 0.6rem; justify-content: flex-end; padding-bottom: 0.6rem;
} }
.hero-icon-big { font-size: 3rem; line-height: 1; margin-bottom: 0.4rem; } .hero-icon-big { line-height: 1; margin-bottom: 0.4rem; }
/* Weather icon images (OWM CDN) */
.wx-icon { display: block; width: 40px; height: 40px; object-fit: contain; }
.wx-icon--hero { width: 80px; height: 80px; }
.wx-icon--sm { width: 32px; height: 32px; }
.hero-stats-mini { display: flex; flex-direction: column; gap: 0.2rem; } .hero-stats-mini { display: flex; flex-direction: column; gap: 0.2rem; }
.hero-stats-mini span { font-size: 0.85rem; color: var(--muted2); } .hero-stats-mini span { font-size: 0.85rem; color: var(--muted2); }
@@ -410,7 +415,7 @@ main { flex: 1; }
.hcard-time { font-size: 0.85rem; font-weight: 600; color: var(--text); } .hcard-time { font-size: 0.85rem; font-weight: 600; color: var(--text); }
.hcard-date { font-size: 0.68rem; color: var(--muted); margin-top: -0.15rem; } .hcard-date { font-size: 0.68rem; color: var(--muted); margin-top: -0.15rem; }
.hcard-icon { font-size: 1.4rem; line-height: 1; margin: 0.2rem 0; } .hcard-icon { line-height: 1; margin: 0.2rem 0; display: flex; justify-content: center; }
.hcard-temp { font-size: 1rem; font-weight: 700; } .hcard-temp { font-size: 1rem; font-weight: 700; }
.hcard-precip { font-size: 0.7rem; color: var(--blue); font-weight: 500; } .hcard-precip { font-size: 0.7rem; color: var(--blue); font-weight: 500; }
.hcard-precip--none { color: var(--muted); font-weight: 400; } .hcard-precip--none { color: var(--muted); font-weight: 400; }
@@ -480,7 +485,7 @@ main { flex: 1; }
.drow:hover { background: rgba(255,255,255,0.05); } .drow:hover { background: rgba(255,255,255,0.05); }
.drow-left { display: flex; align-items: center; gap: 0.6rem; } .drow-left { display: flex; align-items: center; gap: 0.6rem; }
.drow-icon { font-size: 1.4rem; flex-shrink: 0; } .drow-icon { font-size: 1.4rem; flex-shrink: 0; display: flex; align-items: center; }
.drow-dow { font-size: 0.875rem; font-weight: 600; color: var(--text); } .drow-dow { font-size: 0.875rem; font-weight: 600; color: var(--text); }
.drow-bar-wrap { position: relative; } .drow-bar-wrap { position: relative; }

View File

@@ -27,7 +27,9 @@
<div class="hero-main"> <div class="hero-main">
<div class="hero-temp">{{ current.temp_c if current.temp_c is not none else "" }}°</div> <div class="hero-temp">{{ current.temp_c if current.temp_c is not none else "" }}°</div>
<div class="hero-desc"> <div class="hero-desc">
<div class="hero-icon-big">{{ current.icon or "☁️" }}</div> <div class="hero-icon-big">
<img class="wx-icon wx-icon--hero" src="{{ icon_url(current.icon) }}" alt="{{ current.icon }}">
</div>
<div class="hero-stats-mini"> <div class="hero-stats-mini">
{% if current.wind_kmh is not none %} {% if current.wind_kmh is not none %}
<span>{{ current.wind_kmh }} km/h {{ wind_dir_name(current.wind_dir) }}</span> <span>{{ current.wind_kmh }} km/h {{ wind_dir_name(current.wind_dir) }}</span>
@@ -168,7 +170,9 @@
{% if h.datetime is string %}{{ h.datetime[8:10] }}.{{ h.datetime[5:7] }}. {% if h.datetime is string %}{{ h.datetime[8:10] }}.{{ h.datetime[5:7] }}.
{% else %}{{ h.datetime.strftime('%d.%m.') }}{% endif %} {% else %}{{ h.datetime.strftime('%d.%m.') }}{% endif %}
</div> </div>
<div class="hcard-icon">{{ h.icon }}</div> <div class="hcard-icon">
<img class="wx-icon" src="{{ icon_url(h.icon) }}" alt="{{ h.icon }}">
</div>
<div class="hcard-temp"> <div class="hcard-temp">
{% if h.temp_c is not none %}{{ h.temp_c }}°{% else %}{% endif %} {% if h.temp_c is not none %}{{ h.temp_c }}°{% else %}{% endif %}
</div> </div>
@@ -226,7 +230,9 @@
{% for d in daily %} {% for d in daily %}
<div class="drow"> <div class="drow">
<div class="drow-left"> <div class="drow-left">
<span class="drow-icon">{{ d.icon }}</span> <span class="drow-icon">
<img class="wx-icon wx-icon--sm" src="{{ icon_url(d.icon) }}" alt="{{ d.icon }}">
</span>
<div class="drow-date-wrap"> <div class="drow-date-wrap">
<span class="drow-dow"> <span class="drow-dow">
{% if d.date is string %} {% if d.date is string %}