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>
This commit is contained in:
co-authored by
Hermes
parent
81e46a2c60
commit
45d854e85e
+34
-40
@@ -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
|
import os, sys, json, math
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import urllib.request, urllib.error
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
from plugins.base import Widget
|
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
|
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):
|
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?"
|
url = (f"https://api.open-meteo.com/v1/forecast?"
|
||||||
f"latitude={lat}&longitude={lon}"
|
f"latitude={lat}&longitude={lon}"
|
||||||
f"¤t=temperature_2m,relative_humidity_2m,weather_code,"
|
f"¤t=temperature_2m,relative_humidity_2m,weather_code,"
|
||||||
f"wind_speed_10m,wind_direction_10m,surface_pressure,uv_index,is_day"
|
f"wind_speed_10m,wind_direction_10m,surface_pressure,uv_index,is_day"
|
||||||
f"&hourly=temperature_2m,weather_code"
|
f"&hourly=temperature_2m,weather_code"
|
||||||
f"&forecast_days=2&timezone=auto")
|
f"&forecast_days=2&timezone=auto")
|
||||||
|
raw, err = fetch_url(url, timeout=8, retries=3)
|
||||||
|
if err:
|
||||||
|
return None, err
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(url, timeout=8) as r:
|
return json.loads(raw), None
|
||||||
return json.loads(r.read())
|
except json.JSONDecodeError as e:
|
||||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e:
|
return None, f"Datenfehler: {e}"
|
||||||
return {"_error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
class Widget(Widget):
|
class Widget(Widget):
|
||||||
@@ -45,9 +48,10 @@ class Widget(Widget):
|
|||||||
lat = float(parts[0]); lon = float(parts[1])
|
lat = float(parts[0]); lon = float(parts[1])
|
||||||
except Exception:
|
except Exception:
|
||||||
lat, lon = 52.52, 13.41
|
lat, lon = 52.52, 13.41
|
||||||
data = _fetch_openmeteo(lat, lon)
|
data, err = _fetch_openmeteo(lat, lon)
|
||||||
if "_error" not in data:
|
if err:
|
||||||
data["_lat"] = lat; data["_lon"] = lon
|
return {"_error": err}
|
||||||
|
data["_lat"] = lat; data["_lon"] = lon
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _draw_compass(self, draw, cx, cy, r, deg, fonts):
|
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)
|
draw.ellipse((cx-3, cy-3, cx+3, cy+3), fill=FG)
|
||||||
|
|
||||||
def render(self, draw, fonts, x, y, w, h):
|
def render(self, draw, fonts, x, y, w, h):
|
||||||
from palette import fill_for
|
from palette import measure
|
||||||
pad = 8
|
|
||||||
d = self.fetch()
|
d = self.fetch()
|
||||||
|
|
||||||
if "_error" in d:
|
if "_error" in d:
|
||||||
draw.text((x + pad, y + pad), "WETTER", font=fonts.get("24", fonts.get("20")), fill=INFO)
|
render_error_banner(draw, fonts, x, y, w, h,
|
||||||
draw.text((x + pad, y + 60), f"Fehler: {d['_error'][:40]}", font=fonts.get("20", fonts.get("16")), fill=ALERT)
|
self.label, d["_error"])
|
||||||
return
|
return
|
||||||
|
|
||||||
cur = d.get("current", {})
|
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
|
temp_color = RED if temp >= 28 else ORANGE if temp >= 20 else BLUE if temp <= 5 else FG
|
||||||
|
|
||||||
if is_small(w, h):
|
if is_small(w, h):
|
||||||
# mini: nur Big Temp + ggf UV
|
|
||||||
temp_str = f"{int(temp + 0.5)}°"
|
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)
|
centered_text(draw, temp_str, x, y, w, int(h * 0.6), font, temp_color)
|
||||||
if self.cfg("show_uv"):
|
if self.cfg("show_uv"):
|
||||||
uv_int = int(cur.get("uv_index", 0) + 0.5)
|
uv_int = int(cur.get("uv_index", 0) + 0.5)
|
||||||
uv_str = f"UV {uv_int}"
|
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)
|
centered_text(draw, uv_str, x, y + int(h * 0.62), w, h // 4, font_uv, FG)
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_wide(w, h):
|
if is_wide(w, h):
|
||||||
# Wide strip: Temp + Humidity + Wind
|
|
||||||
temp_str = f"{int(temp + 0.5)}°"
|
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)
|
centered_text(draw, temp_str, x, y, w // 3, h, font, temp_color)
|
||||||
hum = cur.get("relative_humidity_2m", 0)
|
hum = cur.get("relative_humidity_2m", 0)
|
||||||
ws = cur.get("wind_speed_10m", 0)
|
ws = cur.get("wind_speed_10m", 0)
|
||||||
wd = cur.get("wind_direction_10m", 0)
|
wd = cur.get("wind_direction_10m", 0)
|
||||||
stats = f"Hum {hum}%\nWind {ws} km/h\nDir {wd}°"
|
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)
|
font_s = fit_font(draw, "Hum 100%", fonts, w - w // 3 - 2 * 8, h // 3)
|
||||||
y_off = y + pad
|
y_off = y + 8
|
||||||
for line in stats.split("\n"):
|
for line in stats.split("\n"):
|
||||||
centered_text(draw, line, x + w // 3, y_off, w - w // 3, h // 3, font_s, FG)
|
centered_text(draw, line, x + w // 3, y_off, w - w // 3, h // 3, font_s, FG)
|
||||||
y_off += h // 3
|
y_off += h // 3
|
||||||
return
|
return
|
||||||
|
|
||||||
# Standard: Big temp, info, forecast
|
# Standard: Big temp, info, forecast
|
||||||
# Header
|
draw.text((x + 8, y + 8), "WETTER", font=fonts.get("24", fonts.get("20")), fill=INFO)
|
||||||
draw.text((x + pad, y + pad), "WETTER", font=fonts.get("24", fonts.get("20")), fill=INFO)
|
|
||||||
# Big temp
|
|
||||||
temp_str = f"{int(temp + 0.5)}°"
|
temp_str = f"{int(temp + 0.5)}°"
|
||||||
font = fit_font(draw, temp_str, fonts, w // 2 - 2 * pad, int(h * 0.6))
|
font = fit_font(draw, temp_str, fonts, w // 2 - 2 * 8, int(h * 0.6))
|
||||||
draw.text((x + pad, y + 50), temp_str, font=font, fill=temp_color)
|
draw.text((x + 8, y + 50), temp_str, font=font, fill=temp_color)
|
||||||
|
|
||||||
# Stats right
|
|
||||||
hum = cur.get("relative_humidity_2m", 0)
|
hum = cur.get("relative_humidity_2m", 0)
|
||||||
ws = cur.get("wind_speed_10m", 0)
|
ws = cur.get("wind_speed_10m", 0)
|
||||||
wd = cur.get("wind_direction_10m", 0)
|
wd = cur.get("wind_direction_10m", 0)
|
||||||
stats_y = y + 60
|
stats_y = y + 60
|
||||||
for line, col in [(f"Hum {hum}%", BLUE), (f"Wind {ws} km/h", INFO), (f"Dir {wd}°", FG)]:
|
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)
|
font_l = fit_font(draw, line, fonts, w // 2 - 2 * 8, 24)
|
||||||
draw.text((x + w // 2 + pad, stats_y), line, font=font_l, fill=col)
|
draw.text((x + w // 2 + 8, stats_y), line, font=font_l, fill=col)
|
||||||
stats_y += 30
|
stats_y += 30
|
||||||
|
|
||||||
# UV box top right
|
|
||||||
if self.cfg("show_uv"):
|
if self.cfg("show_uv"):
|
||||||
uv = cur.get("uv_index", 0)
|
uv = cur.get("uv_index", 0)
|
||||||
uv_int = int(uv + 0.5)
|
uv_int = int(uv + 0.5)
|
||||||
box_w = min(80, w // 5)
|
box_w = min(80, w // 5)
|
||||||
box_x = x + w - box_w - pad
|
box_x = x + w - box_w - 8
|
||||||
box_y = y + pad
|
box_y = y + 8
|
||||||
box_h = 36
|
box_h = 36
|
||||||
if uv_int >= 6:
|
if uv_int >= 6:
|
||||||
draw.rectangle((box_x, box_y, box_x + box_w, box_y + box_h), fill=ALERT)
|
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:
|
else:
|
||||||
draw.rectangle((box_x, box_y, box_x + box_w, box_y + box_h), outline=FG, width=2)
|
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)
|
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:
|
if self.cfg("show_wind_compass") and w >= 280 and h >= 280:
|
||||||
cr = min(60, w // 8, h // 6)
|
cr = min(60, w // 8, h // 6)
|
||||||
cx = x + w - cr - pad
|
cx = x + w - cr - 8
|
||||||
cy = y + h - cr - pad
|
cy = y + h - cr - 8
|
||||||
self._draw_compass(draw, cx, cy, cr, wd, fonts)
|
self._draw_compass(draw, cx, cy, cr, wd, fonts)
|
||||||
|
|
||||||
# Forecast bottom
|
|
||||||
fc_h = max(1, min(int(self.cfg("show_forecast_hours", 4)), 8))
|
fc_h = max(1, min(int(self.cfg("show_forecast_hours", 4)), 8))
|
||||||
times = d.get("hourly", {}).get("time", [])
|
times = d.get("hourly", {}).get("time", [])
|
||||||
temps = d.get("hourly", {}).get("temperature_2m", [])
|
temps = d.get("hourly", {}).get("temperature_2m", [])
|
||||||
@@ -164,7 +159,6 @@ class Widget(Widget):
|
|||||||
try: idx0 = times.index(cur_iso)
|
try: idx0 = times.index(cur_iso)
|
||||||
except ValueError: idx0 = 0
|
except ValueError: idx0 = 0
|
||||||
forecast_y = y + h - 70
|
forecast_y = y + h - 70
|
||||||
# Forecast-Strip nimmt 60% der Slot-Breite, von links
|
|
||||||
strip_w = int(w * 0.6)
|
strip_w = int(w * 0.6)
|
||||||
cell_w = strip_w // fc_h
|
cell_w = strip_w // fc_h
|
||||||
for i in range(fc_h):
|
for i in range(fc_h):
|
||||||
@@ -173,7 +167,7 @@ class Widget(Widget):
|
|||||||
hh = times[j].split("T")[1][:5]
|
hh = times[j].split("T")[1][:5]
|
||||||
tt = int(temps[j] + 0.5)
|
tt = int(temps[j] + 0.5)
|
||||||
color = ORANGE if tt >= 25 else BLUE if tt <= 5 else FG
|
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)
|
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)
|
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)
|
draw.text((cx2, forecast_y + 22), f"{tt}°", font=font_t, fill=color)
|
||||||
|
|||||||
Reference in New Issue
Block a user