From 0bb1f9bc1e8fbdc99953d72b1aec071302ab657d Mon Sep 17 00:00:00 2001 From: owain Date: Sun, 7 Jun 2026 22:35:38 +0100 Subject: [PATCH] Fetch full VO2 max history via maxmet/daily range query Instead of storing only the most recent measurement, query the maxmet endpoint with the full sync window (start_date to today) to populate one row per measurement date. Falls back to training_status most-recent value if the range query returns nothing. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/garmin_connect_sync.py | 49 ++++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/backend/app/services/garmin_connect_sync.py b/backend/app/services/garmin_connect_sync.py index ccd62fe..598a27e 100644 --- a/backend/app/services/garmin_connect_sync.py +++ b/backend/app/services/garmin_connect_sync.py @@ -325,27 +325,39 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db, logger.warning("Failed to upsert health_metrics for %s: %s", day_str, exc) db.rollback() - # Fetch VO2 max from training status (slow-changing — store against measurement date) + # Fetch historical VO2 max across the full sync window via maxmet/daily range query today_str = date.today().isoformat() - ts_data = _safe(garmin.get_training_status, today_str) fa_data = _safe(garmin.get_fitnessage_data, today_str) - - vo2 = None - vo2_date = today_str - generic = (ts_data or {}).get("mostRecentVO2Max", {}).get("generic") or {} - v = generic.get("vo2MaxPreciseValue") or generic.get("vo2MaxValue") - if v and float(v) > 0: - vo2 = float(v) - vo2_date = generic.get("calendarDate") or today_str - fa_age = None if fa_data: fa_age = fa_data.get("fitnessAge") or fa_data.get("achievableFitnessAge") - if vo2: + try: + mm_history = garmin.connectapi( + f"/metrics-service/metrics/maxmet/daily/{start_date.isoformat()}/{today_str}" + ) + except Exception as exc: + logger.debug("maxmet history fetch failed: %s", exc) + mm_history = [] + + # Fall back to most-recent from training status if history is empty + if not mm_history: + ts_data = _safe(garmin.get_training_status, today_str) + generic = (ts_data or {}).get("mostRecentVO2Max", {}).get("generic") or {} + v = generic.get("vo2MaxPreciseValue") or generic.get("vo2MaxValue") + if v and float(v) > 0: + mm_history = [{"calendarDate": generic.get("calendarDate") or today_str, + "vo2MaxPreciseValue": float(v)}] + + stored = 0 + for entry in (mm_history or []): + v = entry.get("vo2MaxPreciseValue") or entry.get("vo2MaxValue") + if not v or float(v) <= 0: + continue + entry_date = entry.get("calendarDate") or today_str try: - fa_row = {"vo2max": vo2} - if fa_age: + fa_row = {"vo2max": float(v)} + if fa_age and entry_date == today_str: fa_row["fitness_age"] = int(fa_age) fa_cols = list(fa_row.keys()) db.execute(text(f""" @@ -353,13 +365,16 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db, VALUES (:user_id, :day, {", ".join(f":{c}" for c in fa_cols)}) ON CONFLICT (user_id, date) DO UPDATE SET {", ".join(f"{c} = EXCLUDED.{c}" for c in fa_cols)} - """), {"user_id": user_id, "day": vo2_date, **fa_row}) + """), {"user_id": user_id, "day": entry_date, **fa_row}) db.commit() - logger.info("Stored VO2 max %.1f for %s", vo2, vo2_date) + stored += 1 except Exception as exc: - logger.warning("Failed to upsert VO2 max: %s", exc) + logger.warning("Failed to upsert VO2 max for %s: %s", entry_date, exc) db.rollback() + if stored: + logger.info("Stored %d VO2 max data points", stored) + return processed