From 70c7e5c0a82a5ee088dbf24e94dd022c31969c40 Mon Sep 17 00:00:00 2001 From: owain Date: Sun, 7 Jun 2026 23:18:04 +0100 Subject: [PATCH] Fix VO2 max extraction: values nested under entry['generic'] not top-level The maxmet/daily range query returns entries shaped as: {"generic": {"calendarDate": "...", "vo2MaxPreciseValue": 42.7, ...}, ...} The extractor was looking at the top level of each entry, finding nothing, and falling through to the single-point training_status fallback every time. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/garmin_connect_sync.py | 24 +++++++++---------- .../app/services/garmin_connect_sync.py | 21 ++++++++-------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/backend/app/services/garmin_connect_sync.py b/backend/app/services/garmin_connect_sync.py index 57a14e6..3b49958 100644 --- a/backend/app/services/garmin_connect_sync.py +++ b/backend/app/services/garmin_connect_sync.py @@ -342,17 +342,16 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db, len(mm_raw) if isinstance(mm_raw, (list, dict)) else "n/a") if isinstance(mm_raw, list): mm_entries = mm_raw - if mm_entries and isinstance(mm_entries[0], dict): - logger.info("maxmet first entry keys: %s", list(mm_entries[0].keys())) - logger.info("maxmet first entry: %s", mm_entries[0]) except Exception as exc: logger.info("maxmet history fetch failed: %s", exc) - # Check whether the range query yielded any usable vo2max values + # Each entry has the vo2max data nested under entry["generic"] + def _extract_generic(entry): + return (entry.get("generic") or {}) if isinstance(entry, dict) else {} + valid_from_range = any( - (entry.get("vo2MaxPreciseValue") or entry.get("vo2MaxValue") or 0) - for entry in mm_entries - if isinstance(entry, dict) + (_extract_generic(e).get("vo2MaxPreciseValue") or _extract_generic(e).get("vo2MaxValue") or 0) + for e in mm_entries ) # Always fall back to training_status when the range query had no valid data @@ -362,17 +361,16 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db, v = generic.get("vo2MaxPreciseValue") or generic.get("vo2MaxValue") logger.info("training_status vo2max=%s at %s", v, generic.get("calendarDate")) if v and float(v) > 0: - mm_entries = [{"calendarDate": generic.get("calendarDate") or today_str, - "vo2MaxPreciseValue": float(v)}] + mm_entries = [{"generic": {"calendarDate": generic.get("calendarDate") or today_str, + "vo2MaxPreciseValue": float(v)}}] stored = 0 for entry in mm_entries: - if not isinstance(entry, dict): - continue - v = entry.get("vo2MaxPreciseValue") or entry.get("vo2MaxValue") + generic = _extract_generic(entry) + v = generic.get("vo2MaxPreciseValue") or generic.get("vo2MaxValue") if not v or float(v) <= 0: continue - entry_date = entry.get("calendarDate") or today_str + entry_date = generic.get("calendarDate") or today_str try: fa_row = {"vo2max": float(v)} if fa_age and entry_date == today_str: diff --git a/milevault_export/backend/app/services/garmin_connect_sync.py b/milevault_export/backend/app/services/garmin_connect_sync.py index b960ef5..3b49958 100644 --- a/milevault_export/backend/app/services/garmin_connect_sync.py +++ b/milevault_export/backend/app/services/garmin_connect_sync.py @@ -345,11 +345,13 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db, except Exception as exc: logger.info("maxmet history fetch failed: %s", exc) - # Check whether the range query yielded any usable vo2max values + # Each entry has the vo2max data nested under entry["generic"] + def _extract_generic(entry): + return (entry.get("generic") or {}) if isinstance(entry, dict) else {} + valid_from_range = any( - (entry.get("vo2MaxPreciseValue") or entry.get("vo2MaxValue") or 0) - for entry in mm_entries - if isinstance(entry, dict) + (_extract_generic(e).get("vo2MaxPreciseValue") or _extract_generic(e).get("vo2MaxValue") or 0) + for e in mm_entries ) # Always fall back to training_status when the range query had no valid data @@ -359,17 +361,16 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db, v = generic.get("vo2MaxPreciseValue") or generic.get("vo2MaxValue") logger.info("training_status vo2max=%s at %s", v, generic.get("calendarDate")) if v and float(v) > 0: - mm_entries = [{"calendarDate": generic.get("calendarDate") or today_str, - "vo2MaxPreciseValue": float(v)}] + mm_entries = [{"generic": {"calendarDate": generic.get("calendarDate") or today_str, + "vo2MaxPreciseValue": float(v)}}] stored = 0 for entry in mm_entries: - if not isinstance(entry, dict): - continue - v = entry.get("vo2MaxPreciseValue") or entry.get("vo2MaxValue") + generic = _extract_generic(entry) + v = generic.get("vo2MaxPreciseValue") or generic.get("vo2MaxValue") if not v or float(v) <= 0: continue - entry_date = entry.get("calendarDate") or today_str + entry_date = generic.get("calendarDate") or today_str try: fa_row = {"vo2max": float(v)} if fa_age and entry_date == today_str: