From b3b0ca0ce54e3200a009c564e919c12c04a2fdc9 Mon Sep 17 00:00:00 2001 From: Patrik Date: Mon, 31 Aug 2026 01:03:15 +0200 Subject: [PATCH] REFACTOR-PLUGINS: shared helpers + deutsche Strings durchgaengig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- plugins/clock_wordclock.py | 11 ++-- plugins/minimax.py | 9 +-- plugins/strava.py | 20 +++---- plugins/system.py | 13 +++-- plugins/weather.py | 114 ++++++++++++++++++++----------------- 5 files changed, 90 insertions(+), 77 deletions(-) diff --git a/plugins/clock_wordclock.py b/plugins/clock_wordclock.py index b2c71ea..0151b92 100644 --- a/plugins/clock_wordclock.py +++ b/plugins/clock_wordclock.py @@ -21,7 +21,7 @@ import os, sys from datetime import datetime sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from plugins.base import Widget -from palette import FG, BG, OK, BLUE, YELLOW, ORANGE, fill_for, measure +from palette import FG, BG, OK, BLUE, YELLOW, ORANGE, fill_for, measure, de_weekday_long, de_date # ---- Deutsche Minuten-Phrasen (QlockTwo-konform) ---- @@ -174,19 +174,20 @@ class Widget(Widget): prefer=["16", "20", "default"]) if header_h: if show_wd and show_dt: - day = now.strftime("%A") - date = now.strftime("%e. %b %Y").strip() + # Locale-frei: explizit deutsche Wochentage/Datums-Formatter + day = de_weekday_long(now.weekday()) + date = de_date(now, short=True) text = f"{day} · {date}" tw, th = measure(draw, text, header_font) draw.text((inner_x + (inner_w - tw) // 2, cy), text, font=header_font, fill=accent) elif show_wd: - text = now.strftime("%A") + text = de_weekday_long(now.weekday()) tw, _ = measure(draw, text, header_font) draw.text((inner_x + (inner_w - tw) // 2, cy), text, font=header_font, fill=accent) elif show_dt: - text = now.strftime("%e. %b %Y").strip() + text = de_date(now, short=True) tw, _ = measure(draw, text, header_font) draw.text((inner_x + (inner_w - tw) // 2, cy), text, font=header_font, fill=accent) diff --git a/plugins/minimax.py b/plugins/minimax.py index 94907c3..1f2ceed 100644 --- a/plugins/minimax.py +++ b/plugins/minimax.py @@ -8,15 +8,16 @@ from palette import FG, INFO, OK, WARN, ALERT, ORANGE, BG, measure, fit_font, hb def _fmt_timedelta_short(seconds): + """Formatiert Sekunden als deutsche Kurz-Laufzeit: '3T 5h', '7h 12min', '42min', 'jetzt'.""" if seconds is None: return "" s = int(seconds) - if s <= 0: return "now" + if s <= 0: return "jetzt" days, rem = divmod(s, 86400) h, rem = divmod(rem, 3600) m = rem // 60 - if days: return f"{days}d {h}h" - if h: return f"{h}h {m}m" - return f"{m}m" + if days: return f"{days}T {h}h" + if h: return f"{h}h {m}min" + return f"{m}min" def _deep_get(d, *keys, default=None): diff --git a/plugins/strava.py b/plugins/strava.py index d70954d..0a9ff61 100644 --- a/plugins/strava.py +++ b/plugins/strava.py @@ -37,7 +37,7 @@ def _stats(activities): class Widget(Widget): name = "strava" label = "Strava Aktivitäten" - description = "Distanz und Rides aus Strava. Setze Client-ID/Secret/Refresh-Token unten." + description = "Distanz und Fahrten aus Strava. Client-ID/Secret/Refresh-Token unten setzen." category = "fitness" config_schema = [ @@ -77,8 +77,8 @@ class Widget(Widget): return if is_wide(w, h): - stats = [f"{d['year_km']:.0f} km YTD", f"{d['rides']} rides", - f"{d['total_km']:.0f} km total"] + stats = [f"{d['year_km']:.0f} km Jahr", f"{d['rides']} Fahrten", + f"{d['total_km']:.0f} km Gesamt"] col_w = w // len(stats) for i, s in enumerate(stats): cx = x + i * col_w @@ -90,10 +90,10 @@ class Widget(Widget): font_b = fit_font(draw, f"{d['year_km']:.0f} km", fonts, w - 2 * pad, h // 3) draw.text((x + pad, y + 50), f"{d['year_km']:.0f} km", font=font_b, fill=FG) draw.text((x + pad, y + 50 + font_b.size + 8), - f"in {datetime.now().year}", font=fonts.get("24", fonts.get("20")), fill=OK) - font_r = fit_font(draw, f"{d['rides']} rides", fonts, w - 2 * pad, 28) - draw.text((x + pad, y + h - 100), f"{d['rides']} rides", font=font_r, fill=OK) - font_h = fit_font(draw, f"{d['hike_km']:.1f} km hike", fonts, w - 2 * pad, 24) - draw.text((x + pad, y + h - 70), f"{d['hike_km']:.1f} km hike", font=font_h, fill=FG) - font_t = fit_font(draw, f"Total: {d['total_km']:.0f} km", fonts, w - 2 * pad, 20) - draw.text((x + pad, y + h - 40), f"Total: {d['total_km']:.0f} km", font=font_t, fill=FG) + f"im Jahr {datetime.now().year}", font=fonts.get("24", fonts.get("20")), fill=OK) + font_r = fit_font(draw, f"{d['rides']} Fahrten", fonts, w - 2 * pad, 28) + draw.text((x + pad, y + h - 100), f"{d['rides']} Fahrten", font=font_r, fill=OK) + font_h = fit_font(draw, f"{d['hike_km']:.1f} km Wandern", fonts, w - 2 * pad, 24) + draw.text((x + pad, y + h - 70), f"{d['hike_km']:.1f} km Wandern", font=font_h, fill=FG) + font_t = fit_font(draw, f"Gesamt: {d['total_km']:.0f} km", fonts, w - 2 * pad, 20) + draw.text((x + pad, y + h - 40), f"Gesamt: {d['total_km']:.0f} km", font=font_t, fill=FG) diff --git a/plugins/system.py b/plugins/system.py index 3ccd83d..3223a17 100644 --- a/plugins/system.py +++ b/plugins/system.py @@ -34,12 +34,13 @@ def _read_uptime(): def _fmt_uptime(s): + """Formatiert Sekunden als deutsche Laufzeit: '3T 5h', '7h 12min', '42min'.""" days, rem = divmod(int(s), 86400) h, rem = divmod(rem, 3600) m = rem // 60 - if days: return f"{days}d {h}h" - if h: return f"{h}h {m}m" - return f"{m}m" + if days: return f"{days}T {h}h" + if h: return f"{h}h {m}min" + return f"{m}min" def _system_stats(): @@ -122,7 +123,7 @@ class Widget(Widget): draw.text((cx + pad, y + pad), f"{label} {pct}%", font=label_font, fill=col_color) if self.cfg("show_uptime"): - up_str = "up " + _fmt_uptime(s["uptime_s"]) + up_str = "läuft " + _fmt_uptime(s["uptime_s"]) font_up = fit_font(draw, up_str, fonts, col_w - 2 * pad, 22) draw.text((x + 2 * col_w + pad, y + pad), up_str, font=font_up, fill=INFO) return @@ -155,9 +156,9 @@ class Widget(Widget): if self.cfg("show_load") or self.cfg("show_uptime"): parts = [] if self.cfg("show_load"): - parts.append(f"load {s['cpu_pct']/100.0 * (os.cpu_count() or 1):.2f}") + parts.append(f"Last {s['cpu_pct']/100.0 * (os.cpu_count() or 1):.2f}") if self.cfg("show_uptime"): - parts.append("up " + _fmt_uptime(s["uptime_s"])) + parts.append("läuft " + _fmt_uptime(s["uptime_s"])) line = " · ".join(parts) font_bot = fit_font(draw, line, fonts, w - 2 * pad, 22) draw.text((x + pad, bottom_y), line, font=font_bot, fill=FG) diff --git a/plugins/weather.py b/plugins/weather.py index d121bca..1e92900 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -1,10 +1,18 @@ -"""Wetter-Plugin (Open-Meteo, kein API-Key). Responsive + Error-Handling.""" -import os, sys, json, math +"""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 +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) + measure, fit_font, centered_text, is_small, is_wide, + temp_color) def _fetch_openmeteo(lat, lon): @@ -27,7 +35,7 @@ def _fetch_openmeteo(lat, lon): class Widget(Widget): name = "weather" label = "Wetter (Open-Meteo)" - description = "Aktuelles Wetter, Forecast und Windrose. Kein API-Key. Responsive." + description = "Aktuelles Wetter, Vorhersage und Windrose. Kein API-Key. Responsive." category = "weather" config_schema = [ @@ -54,31 +62,7 @@ class Widget(Widget): 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: @@ -88,46 +72,59 @@ class Widget(Widget): 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 + tcol = temp_color(temp) 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) + 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 - 2 * 8, h // 4) + 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 - 2 * 8, h - 2 * 8) - centered_text(draw, temp_str, x, y, w // 3, h, font, temp_color) + 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"Hum {hum}%\nWind {ws} km/h\nDir {wd}°" - font_s = fit_font(draw, "Hum 100%", fonts, w - w // 3 - 2 * 8, h // 3) + 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.split("\n"): + 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 - 2 * 8, int(h * 0.6)) - draw.text((x + 8, y + 50), temp_str, font=font, fill=temp_color) + 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"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) + 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 @@ -140,34 +137,47 @@ class Widget(Widget): 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)) + 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) + 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 - self._draw_compass(draw, cx, cy, cr, wd, fonts) + 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 + try: + idx0 = times.index(cur_iso) + except ValueError: + idx0 = 0 forecast_y = y + h - 70 - strip_w = int(w * 0.6) + 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 + 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 + 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) - 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) + # 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)