Files
epaper-dashboardandHermes 45d854e85e weather.py: nutze fetch_with_retry + render_error_banner
Bei API-Fehler (HTTP 5xx, Timeout, DNS-Fehler, Connection refused) wird
3x retry mit backoff versucht. Bei 3x fail wird ein auffaelliges rotes
"!"-Schild mit Plugin-Name und Fehlermeldung statt leeren Slot gerendert.

base.py: NEU - fetch_url() Helper mit Retry-Logik + Error-Formatting
        + render_error_banner() Funktion fuer Plugin-Slots.

Co-Authored-By: Hermes <noreply@hermes.local>
2026-08-26 22:15:53 +04:00

174 lines
7.9 KiB
Python

"""Wetter-Plugin (Open-Meteo, kein API-Key). Responsive + Error-Handling."""
import os, sys, json, math
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
from palette import (FG, INFO, OK, WARN, ALERT, ORANGE, RED, BLUE,
measure, fit_font, centered_text, is_small, is_wide)
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"&current=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, Forecast 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 _draw_compass(self, draw, cx, cy, r, deg, fonts):
draw.ellipse((cx-r, cy-r, cx+r, cy+r), outline=FG, width=2)
for a in range(0, 360, 45):
rad = math.radians(a - 90)
inner = r - 8 if a % 90 == 0 else r - 4
x1 = cx + inner * math.cos(rad); y1 = cy + inner * math.sin(rad)
x2 = cx + r * math.cos(rad); y2 = cy + r * math.sin(rad)
draw.line((x1, y1, x2, y2), fill=FG, width=2)
f20 = fonts.get("20", fonts.get("16"))
for a, t in [(270,"N"), (0,"E"), (90,"S"), (180,"W")]:
rad = math.radians(a - 90)
tx = cx + (r+12) * math.cos(rad) - 6
ty = cy + (r+12) * math.sin(rad) - 9
draw.text((tx, ty), t, font=f20, fill=FG)
rad_arrow = math.radians(deg - 90)
tip_x = cx + (r - 14) * math.cos(rad_arrow)
tip_y = cy + (r - 14) * math.sin(rad_arrow)
base = math.radians(150)
lx = cx + 18 * math.cos(rad_arrow + base); ly = cy + 18 * math.sin(rad_arrow + base)
rx = cx + 18 * math.cos(rad_arrow - base); ry = cy + 18 * math.sin(rad_arrow - base)
draw.polygon([(tip_x, tip_y), (lx, ly), (rx, ry)], fill=RED)
draw.ellipse((cx-3, cy-3, cx+3, cy+3), fill=FG)
def render(self, draw, fonts, x, y, w, h):
from palette import measure
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)
temp_color = RED if temp >= 28 else ORANGE if temp >= 20 else BLUE if temp <= 5 else FG
if is_small(w, h):
temp_str = f"{int(temp + 0.5)}°"
font = fit_font(draw, temp_str, fonts, w - 2 * 8, int(h * 0.6))
centered_text(draw, temp_str, x, y, w, int(h * 0.6), font, temp_color)
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 - 2 * 8, 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 - 2 * 8, h - 2 * 8)
centered_text(draw, temp_str, x, y, w // 3, h, font, temp_color)
hum = cur.get("relative_humidity_2m", 0)
ws = cur.get("wind_speed_10m", 0)
wd = cur.get("wind_direction_10m", 0)
stats = f"Hum {hum}%\nWind {ws} km/h\nDir {wd}°"
font_s = fit_font(draw, "Hum 100%", fonts, w - w // 3 - 2 * 8, h // 3)
y_off = y + 8
for line in stats.split("\n"):
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)
temp_str = f"{int(temp + 0.5)}°"
font = fit_font(draw, temp_str, fonts, w // 2 - 2 * 8, int(h * 0.6))
draw.text((x + 8, y + 50), temp_str, font=font, fill=temp_color)
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"Hum {hum}%", BLUE), (f"Wind {ws} km/h", INFO), (f"Dir {wd}°", FG)]:
font_l = fit_font(draw, line, fonts, w // 2 - 2 * 8, 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)
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
self._draw_compass(draw, cx, cy, cr, wd, fonts)
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", [])
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.6)
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 = ORANGE if tt >= 25 else BLUE if tt <= 5 else FG
cx2 = x + 8 + i * cell_w
draw.text((cx2, forecast_y), hh, font=fonts.get("20", fonts.get("16")), fill=FG)
font_t = fit_font(draw, f"{tt}°", fonts, cell_w - 4, 40)
draw.text((cx2, forecast_y + 22), f"{tt}°", font=font_t, fill=color)