Sync to Pi: alle Features die live deployed sind
Aus dem Backup und Live-Pull vom Pi (10.11.3.144): - dashboard.py: Grid-Linien nur im freien Hintergrund (nicht durch Widgets) - templates/index.html: komplett redesigned mit Sidebar + Topbar + Toast + Modal - plugins/clock.py: responsive Layout (1x1 bis 4x4) - plugins/system.py, weather.py, minimax.py: mit Threshold-Bars und Color-Variants - plugins/base.py: NEU — fetch_with_retry Helper (3x retry mit backoff) + render_error_banner für fehlgeschlagene API-Plugins (grosses rotes "!" Icon mit Plugin-Name und Fehler statt Crash) Cleanup: Helfer-Chaos (renderer.py/2/3, design_a/b/c.html, clock_classic.py, 23x clock_*.png, alte test_*.py) wurde bereits im vorherigen Commit entfernt. Co-Authored-By: Hermes <noreply@hermes.local>
This commit is contained in:
co-authored by
Hermes
parent
af9a99a379
commit
1f142f5245
@@ -0,0 +1,99 @@
|
||||
"""Strava Stats - responsive."""
|
||||
import os, sys, time, json
|
||||
import urllib.request, urllib.error
|
||||
from datetime import datetime
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from plugins.base import Widget
|
||||
from palette import FG, INFO, OK, WARN, measure, fit_font, centered_text, is_small, is_wide
|
||||
|
||||
|
||||
def _strava_refresh(client_id, client_secret, refresh_token):
|
||||
data = (f"client_id={client_id}&client_secret={client_secret}"
|
||||
f"&grant_type=refresh_token&refresh_token={refresh_token}").encode()
|
||||
req = urllib.request.Request("https://www.strava.com/oauth/token", data=data)
|
||||
with urllib.request.urlopen(req, timeout=8) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _strava_activities(access_token, page=1):
|
||||
url = f"https://www.strava.com/api/v3/athlete/activities?page={page}&per_page=100"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {access_token}"})
|
||||
with urllib.request.urlopen(req, timeout=8) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _stats(activities):
|
||||
total = sum(a.get("distance", 0) for a in activities)
|
||||
rides = sum(1 for a in activities if a.get("type") in ("Ride", "VirtualRide"))
|
||||
hike = sum(a.get("distance", 0) for a in activities if a.get("type") in ("Hike", "Walk"))
|
||||
year = datetime.now().year
|
||||
year_start = datetime(year, 1, 1).timestamp()
|
||||
year_dist = sum(a.get("distance", 0) for a in activities
|
||||
if datetime.strptime(a["start_date"][:19], "%Y-%m-%dT%H:%M:%S").timestamp() >= year_start)
|
||||
return {"total_km": total / 1000, "year_km": year_dist / 1000,
|
||||
"rides": rides, "hike_km": hike / 1000}
|
||||
|
||||
|
||||
class Widget(Widget):
|
||||
name = "strava"
|
||||
label = "Strava Aktivitäten"
|
||||
description = "Distanz und Rides aus Strava. Setze Client-ID/Secret/Refresh-Token unten."
|
||||
category = "fitness"
|
||||
|
||||
config_schema = [
|
||||
{"key": "client_id", "label": "Strava Client ID", "type": "secret"},
|
||||
{"key": "client_secret", "label": "Strava Client Secret", "type": "secret"},
|
||||
{"key": "refresh_token", "label": "Refresh Token", "type": "secret",
|
||||
"help": "Einmaliger OAuth-Token. Plugin holt sich access_tokens on-demand."},
|
||||
]
|
||||
default_config = {"client_id": "", "client_secret": "", "refresh_token": ""}
|
||||
|
||||
def fetch(self):
|
||||
cid = self.cfg("client_id")
|
||||
csec = self.cfg("client_secret")
|
||||
rtok = self.cfg("refresh_token")
|
||||
if not (cid and csec and rtok):
|
||||
return {"_error": "Tokens fehlen"}
|
||||
try:
|
||||
tok = _strava_refresh(cid, csec, rtok)
|
||||
activities = _strava_activities(tok["access_token"], page=1)
|
||||
return _stats(activities)
|
||||
except Exception as e:
|
||||
return {"_error": str(e)}
|
||||
|
||||
def render(self, draw, fonts, x, y, w, h):
|
||||
pad = 8
|
||||
draw.text((x + pad, y + pad), "STRAVA", font=fonts.get("24", fonts.get("20")), fill=INFO)
|
||||
d = self.fetch()
|
||||
if "_error" in d:
|
||||
font = fit_font(draw, "Konfig fehlt", fonts, w - 2 * pad, h - 80)
|
||||
centered_text(draw, "Konfig fehlt", x, y + 50, w, h - 60, font, WARN)
|
||||
return
|
||||
|
||||
if is_small(w, h):
|
||||
txt = f"{d['year_km']:.0f}km"
|
||||
font = fit_font(draw, txt, fonts, w - 2 * pad, h - 2 * pad)
|
||||
centered_text(draw, txt, x, y, w, h, font, OK)
|
||||
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"]
|
||||
col_w = w // len(stats)
|
||||
for i, s in enumerate(stats):
|
||||
cx = x + i * col_w
|
||||
font = fit_font(draw, s, fonts, col_w - 2 * pad, h - 50)
|
||||
centered_text(draw, s, cx, y + 30, col_w, h - 30, font, FG)
|
||||
return
|
||||
|
||||
# Standard
|
||||
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)
|
||||
Reference in New Issue
Block a user