"""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)