From 45d854e85e34c619d2ecfcf5ee659015081d6806 Mon Sep 17 00:00:00 2001 From: epaper-dashboard Date: Wed, 26 Aug 2026 22:15:53 +0400 Subject: [PATCH] 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 --- plugins/weather.py | 74 +++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/plugins/weather.py b/plugins/weather.py index fa63204..d121bca 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -1,24 +1,27 @@ -"""Wetter-Plugin (Open-Meteo, kein API-Key). Responsive.""" +"""Wetter-Plugin (Open-Meteo, kein API-Key). Responsive + Error-Handling.""" import os, sys, json, math from datetime import datetime -import urllib.request, urllib.error sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from plugins.base import Widget -from palette import FG, INFO, OK, WARN, ALERT, ORANGE, RED, BLUE, measure, fit_font, centered_text, is_small, is_wide +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"¤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: - with urllib.request.urlopen(url, timeout=8) as r: - return json.loads(r.read()) - except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e: - return {"_error": str(e)} + return json.loads(raw), None + except json.JSONDecodeError as e: + return None, f"Datenfehler: {e}" class Widget(Widget): @@ -45,9 +48,10 @@ class Widget(Widget): lat = float(parts[0]); lon = float(parts[1]) except Exception: lat, lon = 52.52, 13.41 - data = _fetch_openmeteo(lat, lon) - if "_error" not in data: - data["_lat"] = lat; data["_lon"] = lon + 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): @@ -74,13 +78,12 @@ class Widget(Widget): draw.ellipse((cx-3, cy-3, cx+3, cy+3), fill=FG) def render(self, draw, fonts, x, y, w, h): - from palette import fill_for - pad = 8 + from palette import measure d = self.fetch() if "_error" in d: - draw.text((x + pad, y + pad), "WETTER", font=fonts.get("24", fonts.get("20")), fill=INFO) - draw.text((x + pad, y + 60), f"Fehler: {d['_error'][:40]}", font=fonts.get("20", fonts.get("16")), fill=ALERT) + render_error_banner(draw, fonts, x, y, w, h, + self.label, d["_error"]) return cur = d.get("current", {}) @@ -88,74 +91,66 @@ class Widget(Widget): temp_color = RED if temp >= 28 else ORANGE if temp >= 20 else BLUE if temp <= 5 else FG if is_small(w, h): - # mini: nur Big Temp + ggf UV temp_str = f"{int(temp + 0.5)}°" - font = fit_font(draw, temp_str, fonts, w - 2 * pad, int(h * 0.6)) + 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 * pad, h // 4) + 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): - # Wide strip: Temp + Humidity + Wind temp_str = f"{int(temp + 0.5)}°" - font = fit_font(draw, temp_str, fonts, w // 3 - 2 * pad, h - 2 * pad) + 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 * pad, h // 3) - y_off = y + pad + 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 - # Header - draw.text((x + pad, y + pad), "WETTER", font=fonts.get("24", fonts.get("20")), fill=INFO) - # Big temp + 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 * pad, int(h * 0.6)) - draw.text((x + pad, y + 50), temp_str, font=font, fill=temp_color) + 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) - # Stats right 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 * pad, 24) - draw.text((x + w // 2 + pad, stats_y), line, font=font_l, fill=col) + 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 - # UV box top right 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 - pad - box_y = y + pad + 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=BG) + 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) - # Compass optional if self.cfg("show_wind_compass") and w >= 280 and h >= 280: cr = min(60, w // 8, h // 6) - cx = x + w - cr - pad - cy = y + h - cr - pad + cx = x + w - cr - 8 + cy = y + h - cr - 8 self._draw_compass(draw, cx, cy, cr, wd, fonts) - # Forecast bottom 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", []) @@ -164,7 +159,6 @@ class Widget(Widget): try: idx0 = times.index(cur_iso) except ValueError: idx0 = 0 forecast_y = y + h - 70 - # Forecast-Strip nimmt 60% der Slot-Breite, von links strip_w = int(w * 0.6) cell_w = strip_w // fc_h for i in range(fc_h): @@ -173,7 +167,7 @@ class Widget(Widget): 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 + pad + i * cell_w + 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)