weather.py:
- Custom _draw_compass (~50 Zeilen) entfernt, nutzt jetzt draw_compass
aus plugins/base.py (mit color=RED statt fonts-Argument-Bug)
- Custom temp_color-Logik (ORANGE/BLU/FG) ersetzt durch temp_color() aus palette
- Stunden-Vorhersage zeigt jetzt Wetter-Icon pro Zelle via draw_weather_icon
- Auch im compact-mode: Wetter-Icon oben rechts
- Strings: 'Hum' -> 'Feuchte', 'Dir' -> 'Richtung', 'Aktuelles Wetter' -> 'Vorhersage'
strava.py:
- Doppeltes label='Strava Aktivitäten' am Dateiende entfernt (mojibake-Copy-Paste)
- description: 'Rides' -> 'Fahrten', 'Setze ... unten' sauberer
- Strings: 'rides' -> 'Fahrten' (3x), 'hike' -> 'Wandern' (2x),
'YTD' -> 'Jahr', 'in {year}' -> 'im Jahr {year}', 'Total' -> 'Gesamt'
system.py:
- _fmt_uptime: 'd/h/m' -> 'T/h/min' (deutsche Tages-Notation)
- Strings: 'up ' -> 'laeuft ' (in wide + standard), 'load ' -> 'Last '
minimax.py:
- _fmt_timedelta_short: 'now' -> 'jetzt', 'Xd Yh' -> 'XT Yh', 'Ym' -> 'Ymin'
(deutsche Kurz-Laufzeit, konsistent mit system.py)
clock_wordclock.py:
- Locale-abhaengige strftime('%A') / strftime('%e. %b %Y') durch
de_weekday_long() / de_date() aus palette ersetzt
- Vorher: auf nicht-de_DE-Systemen wurde 'Wednesday' statt 'Mittwoch' angezeigt
Verifikation: alle 9 Plugins importieren sauber, weather rendert mit
shared helpers (docs/test_weather.png).
184 lines
7.9 KiB
Python
184 lines
7.9 KiB
Python
"""Wetter-Plugin (Open-Meteo, kein API-Key). Responsive + Error-Handling.
|
|
|
|
Nutzt die shared helper aus plugins.base und palette:
|
|
- draw_compass, draw_weather_icon, draw_mini_bar
|
|
- temp_color (Hot/Warm/Kalt/FG)
|
|
- render_error_banner
|
|
"""
|
|
import os, sys, json
|
|
from datetime import datetime
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
from plugins.base import (Widget, fetch_url, render_error_banner,
|
|
draw_compass, draw_weather_icon)
|
|
from palette import (FG, INFO, OK, WARN, ALERT, ORANGE, RED, BLUE,
|
|
measure, fit_font, centered_text, is_small, is_wide,
|
|
temp_color)
|
|
|
|
|
|
def _fetch_openmeteo(lat, lon):
|
|
"""Hole Wetterdaten von Open-Meteo mit 3x retry. Returns (data, error)."""
|
|
url = (f"https://api.open-meteo.com/v1/forecast?"
|
|
f"latitude={lat}&longitude={lon}"
|
|
f"¤t=temperature_2m,relative_humidity_2m,weather_code,"
|
|
f"wind_speed_10m,wind_direction_10m,surface_pressure,uv_index,is_day"
|
|
f"&hourly=temperature_2m,weather_code"
|
|
f"&forecast_days=2&timezone=auto")
|
|
raw, err = fetch_url(url, timeout=8, retries=3)
|
|
if err:
|
|
return None, err
|
|
try:
|
|
return json.loads(raw), None
|
|
except json.JSONDecodeError as e:
|
|
return None, f"Datenfehler: {e}"
|
|
|
|
|
|
class Widget(Widget):
|
|
name = "weather"
|
|
label = "Wetter (Open-Meteo)"
|
|
description = "Aktuelles Wetter, Vorhersage und Windrose. Kein API-Key. Responsive."
|
|
category = "weather"
|
|
|
|
config_schema = [
|
|
{"key": "location", "label": "Standort (lat,lon)", "type": "lat_lon",
|
|
"default": "52.52,13.41", "help": "Beispiel: 52.52,13.41 für Berlin"},
|
|
{"key": "show_uv", "label": "UV-Index anzeigen", "type": "bool", "default": True},
|
|
{"key": "show_forecast_hours", "label": "Vorhersage-Stunden", "type": "int",
|
|
"default": 4, "help": "Wie viele Stunden voraus (1-8)"},
|
|
{"key": "show_wind_compass", "label": "Windrose anzeigen", "type": "bool", "default": True},
|
|
]
|
|
default_config = {"location": "52.52,13.41", "show_uv": True,
|
|
"show_forecast_hours": 4, "show_wind_compass": True}
|
|
|
|
def fetch(self):
|
|
loc = self.cfg("location", "52.52,13.41")
|
|
try:
|
|
parts = [s.strip() for s in loc.split(",")]
|
|
lat = float(parts[0]); lon = float(parts[1])
|
|
except Exception:
|
|
lat, lon = 52.52, 13.41
|
|
data, err = _fetch_openmeteo(lat, lon)
|
|
if err:
|
|
return {"_error": err}
|
|
data["_lat"] = lat; data["_lon"] = lon
|
|
return data
|
|
|
|
def render(self, draw, fonts, x, y, w, h):
|
|
d = self.fetch()
|
|
|
|
if "_error" in d:
|
|
render_error_banner(draw, fonts, x, y, w, h,
|
|
self.label, d["_error"])
|
|
return
|
|
|
|
cur = d.get("current", {})
|
|
temp = cur.get("temperature_2m", 0)
|
|
tcol = temp_color(temp)
|
|
|
|
if is_small(w, h):
|
|
temp_str = f"{int(temp + 0.5)}°"
|
|
font = fit_font(draw, temp_str, fonts, w - 16, int(h * 0.6))
|
|
centered_text(draw, temp_str, x, y, w, int(h * 0.6), font, tcol)
|
|
# Wetter-Icon klein oben rechts
|
|
code = cur.get("weather_code")
|
|
if code is not None:
|
|
icon_size = min(20, h // 4)
|
|
draw_weather_icon(draw, x + w - icon_size - 4, y + 4,
|
|
icon_size, code)
|
|
if self.cfg("show_uv"):
|
|
uv_int = int(cur.get("uv_index", 0) + 0.5)
|
|
uv_str = f"UV {uv_int}"
|
|
font_uv = fit_font(draw, uv_str, fonts, w - 16, h // 4)
|
|
centered_text(draw, uv_str, x, y + int(h * 0.62), w, h // 4, font_uv, FG)
|
|
return
|
|
|
|
if is_wide(w, h):
|
|
temp_str = f"{int(temp + 0.5)}°"
|
|
font = fit_font(draw, temp_str, fonts, w // 3 - 16, h - 16)
|
|
centered_text(draw, temp_str, x, y, w // 3, h, font, tcol)
|
|
hum = cur.get("relative_humidity_2m", 0)
|
|
ws = cur.get("wind_speed_10m", 0)
|
|
wd = cur.get("wind_direction_10m", 0)
|
|
stats = [f"Feuchte {hum}%", f"Wind {ws} km/h", f"Richtung {int(wd + 0.5)}°"]
|
|
font_s = fit_font(draw, "Feuchte 100%", fonts, w - w // 3 - 16, h // 3)
|
|
y_off = y + 8
|
|
for line in stats:
|
|
centered_text(draw, line, x + w // 3, y_off, w - w // 3, h // 3, font_s, FG)
|
|
y_off += h // 3
|
|
return
|
|
|
|
# Standard: Big temp, info, forecast
|
|
draw.text((x + 8, y + 8), "WETTER", font=fonts.get("24", fonts.get("20")), fill=INFO)
|
|
# Wetter-Icon oben rechts neben dem Header
|
|
code = cur.get("weather_code")
|
|
if code is not None:
|
|
icon_size = min(28, h // 5)
|
|
draw_weather_icon(draw, x + 8, y + 36, icon_size, code)
|
|
temp_str = f"{int(temp + 0.5)}°"
|
|
font = fit_font(draw, temp_str, fonts, w // 2 - 16, int(h * 0.6))
|
|
draw.text((x + 8, y + 50), temp_str, font=font, fill=tcol)
|
|
|
|
hum = cur.get("relative_humidity_2m", 0)
|
|
ws = cur.get("wind_speed_10m", 0)
|
|
wd = cur.get("wind_direction_10m", 0)
|
|
stats_y = y + 60
|
|
for line, col in [(f"Feuchte {hum}%", BLUE),
|
|
(f"Wind {ws} km/h", INFO),
|
|
(f"Richtung {int(wd + 0.5)}°", FG)]:
|
|
font_l = fit_font(draw, line, fonts, w // 2 - 16, 24)
|
|
draw.text((x + w // 2 + 8, stats_y), line, font=font_l, fill=col)
|
|
stats_y += 30
|
|
|
|
if self.cfg("show_uv"):
|
|
uv = cur.get("uv_index", 0)
|
|
uv_int = int(uv + 0.5)
|
|
box_w = min(80, w // 5)
|
|
box_x = x + w - box_w - 8
|
|
box_y = y + 8
|
|
box_h = 36
|
|
if uv_int >= 6:
|
|
draw.rectangle((box_x, box_y, box_x + box_w, box_y + box_h), fill=ALERT)
|
|
draw.text((box_x + 6, box_y + 4), f"UV {uv_int}",
|
|
font=fonts.get("20", fonts.get("16")), fill=(245, 244, 240))
|
|
else:
|
|
draw.rectangle((box_x, box_y, box_x + box_w, box_y + box_h), outline=FG, width=2)
|
|
draw.text((box_x + 6, box_y + 4), f"UV {uv_int}",
|
|
font=fonts.get("20", fonts.get("16")), fill=FG)
|
|
|
|
# Windrose via shared helper (nur in grossen Slots)
|
|
if self.cfg("show_wind_compass") and w >= 280 and h >= 280:
|
|
cr = min(60, w // 8, h // 6)
|
|
cx = x + w - cr - 8
|
|
cy = y + h - cr - 8
|
|
draw_compass(draw, cx, cy, cr, wd, color=RED)
|
|
|
|
# Stunden-Vorhersage
|
|
fc_h = max(1, min(int(self.cfg("show_forecast_hours", 4)), 8))
|
|
times = d.get("hourly", {}).get("time", [])
|
|
temps = d.get("hourly", {}).get("temperature_2m", [])
|
|
codes = d.get("hourly", {}).get("weather_code", [])
|
|
if times and h > 200:
|
|
cur_iso = datetime.now().strftime("%Y-%m-%dT%H:00")
|
|
try:
|
|
idx0 = times.index(cur_iso)
|
|
except ValueError:
|
|
idx0 = 0
|
|
forecast_y = y + h - 70
|
|
strip_w = int(w * 0.7)
|
|
cell_w = strip_w // fc_h
|
|
for i in range(fc_h):
|
|
j = idx0 + i + 1
|
|
if j >= len(times):
|
|
break
|
|
hh = times[j].split("T")[1][:5]
|
|
tt = int(temps[j] + 0.5)
|
|
color = temp_color(tt)
|
|
cx2 = x + 8 + i * cell_w
|
|
draw.text((cx2, forecast_y), hh, font=fonts.get("20", fonts.get("16")), fill=FG)
|
|
# Wetter-Icon (klein) pro Stunde
|
|
fc_code = codes[j] if j < len(codes) else None
|
|
if fc_code is not None:
|
|
ic_size = min(18, cell_w - 4, 20)
|
|
draw_weather_icon(draw, cx2, forecast_y + 18, ic_size, fc_code)
|
|
font_t = fit_font(draw, f"{tt}°", fonts, cell_w - 4, 28)
|
|
draw.text((cx2, forecast_y + 38), f"{tt}°", font=font_t, fill=color)
|