From 367ae4e8f77de89f2bb8f8b0f59e943e61a8ef66 Mon Sep 17 00:00:00 2001 From: owain Date: Sun, 7 Jun 2026 21:38:41 +0100 Subject: [PATCH] Switch VO2 max source to get_max_metrics (maxmet/daily endpoint) fitnessage endpoint contains fitness age only, not VO2 max. The maxmet endpoint (/metrics-service/metrics/maxmet/daily) is the correct source. Keep debug logging temporarily to confirm key names from live API. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/garmin_connect_sync.py | 69 ++++++++++++++------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/backend/app/services/garmin_connect_sync.py b/backend/app/services/garmin_connect_sync.py index acfd336..0af5394 100644 --- a/backend/app/services/garmin_connect_sync.py +++ b/backend/app/services/garmin_connect_sync.py @@ -325,32 +325,57 @@ 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 current VO2 max and fitness age once (slow-changing — only update today's row) + # Fetch current VO2 max once via maxmet endpoint (slow-changing — only update today's row) today_str = date.today().isoformat() + mm_data = _safe(garmin.get_max_metrics, today_str) + logger.info("maxmet raw response: %s", mm_data) fa_data = _safe(garmin.get_fitnessage_data, today_str) logger.info("fitnessage raw response: %s", fa_data) + + vo2 = None + if mm_data: + # maxmet returns a list of metric dicts or a wrapper + items = mm_data if isinstance(mm_data, list) else mm_data.get("allMetrics", {}).get("metricsMap", {}) + if isinstance(items, list): + for item in items: + for key in ("vo2MaxPreciseValue", "vo2Max", "generic"): + v = item.get(key) + if v and isinstance(v, (int, float)) and float(v) > 0: + vo2 = float(v) + break + if vo2: + break + elif isinstance(items, dict): + for key in ("VO2_MAX", "vo2Max", "vo2MaxPreciseValue"): + entry = items.get(key) + if entry: + v = entry[0].get("value") if isinstance(entry, list) else entry.get("value") + if v and float(v) > 0: + vo2 = float(v) + break + + fa_age = None if fa_data: - vo2 = (fa_data.get("vo2Max") - or fa_data.get("vo2MaxPreciseValue") - or fa_data.get("biometricProfile", {}).get("vo2Max")) - fa = fa_data.get("chronologicalAge") or fa_data.get("fitnessAge") - logger.info("fitnessage parsed: vo2=%s fa=%s", vo2, fa) - if vo2 and float(vo2) > 0: - try: - fa_row = {"vo2max": float(vo2)} - if fa: - fa_row["fitness_age"] = int(fa) - fa_cols = list(fa_row.keys()) - db.execute(text(f""" - INSERT INTO health_metrics (user_id, date, {", ".join(fa_cols)}) - 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": today_str, **fa_row}) - db.commit() - except Exception as exc: - logger.warning("Failed to upsert VO2 max: %s", exc) - db.rollback() + fa_age = fa_data.get("fitnessAge") or fa_data.get("achievableFitnessAge") + + logger.info("parsed vo2=%s fitness_age=%s", vo2, fa_age) + + if vo2: + try: + fa_row = {"vo2max": vo2} + if fa_age: + fa_row["fitness_age"] = int(fa_age) + fa_cols = list(fa_row.keys()) + db.execute(text(f""" + INSERT INTO health_metrics (user_id, date, {", ".join(fa_cols)}) + 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": today_str, **fa_row}) + db.commit() + except Exception as exc: + logger.warning("Failed to upsert VO2 max: %s", exc) + db.rollback() return processed