From 866825b84030b711c477b2f3a325d7b1b9d67677 Mon Sep 17 00:00:00 2001 From: Lubo Molent <23703392+lmolent@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:17:26 +0100 Subject: [PATCH 1/3] Add Sleep Need and Sleep Factors metrics --- scripts/sync_garmin.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/sync_garmin.py b/scripts/sync_garmin.py index 61bb209..eaaf2f5 100644 --- a/scripts/sync_garmin.py +++ b/scripts/sync_garmin.py @@ -5,6 +5,7 @@ """Sync daily health data from Garmin Connect into markdown files.""" import argparse +import re import sys import time from datetime import date, timedelta @@ -175,6 +176,39 @@ def fetch_sleep(client: Garmin, day: str) -> str | None: if score is not None: lines.append(f"Sleep Score: {score}") + sleep_need = daily.get("sleepNeed") + if sleep_need: + # sleepNeed can be a dict (e.g. {'actual': 480, ...}) or just a number + # Note: 'actual' seems to be in minutes, fmt_duration expects seconds + if isinstance(sleep_need, dict): + # Try common keys + val = sleep_need.get("actual") or sleep_need.get("value") or sleep_need.get("duration") + if val is not None: + # If value is small (e.g. 480), assume minutes -> convert to seconds + if val < 1440: # less than 24 hours in minutes + val *= 60 + lines.append(f"Sleep Need: {fmt_duration(val)}") + else: + # If it's just a number, assume seconds if large, minutes if small? + # For now, let's assume seconds if it matches existing behavior or fix if needed + lines.append(f"Sleep Need: {fmt_duration(sleep_need)}") + + # Extract all sleep factors (Alcohol, Caffeine, Late Meal, Stress, Recovery, etc.) + factors = daily.get("sleepScores", {}).get("overall", {}).get("factors", []) + factor_parts = [] + for f in factors: + key = f.get("factorKey") + if not key: + continue + + status = f.get("status", "").replace("_", " ").title() + # Format key: 'sleepDuration' -> 'Sleep Duration' or 'ALCOHOL' -> 'Alcohol' + display_key = re.sub(r'([a-z])([A-Z])', r'\1 \2', key).replace('_', ' ').title() + factor_parts.append(f"{display_key}: {status}") + + if factor_parts: + lines.append("Sleep Factors: " + " | ".join(factor_parts)) + return "\n".join(lines) From 86bfdce8a5564753aad814b510a6bb8f44b2ee4f Mon Sep 17 00:00:00 2001 From: Lubo Molent <23703392+lmolent@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:17:52 +0100 Subject: [PATCH 2/3] Add Lifestyle metrics (Alcohol, Caffeine, etc.) --- scripts/sync_garmin.py | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/scripts/sync_garmin.py b/scripts/sync_garmin.py index eaaf2f5..bc262dc 100644 --- a/scripts/sync_garmin.py +++ b/scripts/sync_garmin.py @@ -212,6 +212,59 @@ def fetch_sleep(client: Garmin, day: str) -> str | None: return "\n".join(lines) +def fetch_lifestyle(client: Garmin, day: str) -> str | None: + """Fetch and format lifestyle logging data (Alcohol, Caffeine, Late Meal, Illness, etc.).""" + try: + # get_lifestyle_logging_data was added in garminconnect 0.2.35 + data = client.get_lifestyle_logging_data(day) + except Exception as e: + if VERBOSE: + print(f" [verbose] Lifestyle logging fetch failed: {e}", file=sys.stderr) + return None + + if VERBOSE: + import json + print(f" [verbose] Raw Lifestyle Data for {day}: {json.dumps(data, indent=2)}", file=sys.stderr) + + if not data: + return None + + # Lifestyle data is in dailyLogsReport + logs = data.get("dailyLogsReport", []) + if not logs: + return None + + lines = ["## Lifestyle"] + for log in logs: + # Only include things that were logged as "YES" + if log.get("logStatus") != "YES": + continue + + name = log.get("name") or str(log.get("behaviourId", "")) + + # Check for amounts in details + details = log.get("details", []) + detail_strs = [] + for d in details: + amount = d.get("amount") + sub_type = d.get("subTypeName") + if amount is not None: + if sub_type: + detail_strs.append(f"{amount} {sub_type.lower()}") + else: + detail_strs.append(f"{amount}") + + line = f"- {name}" + if detail_strs: + line += f": {', '.join(detail_strs)}" + lines.append(line) + + if len(lines) == 1: + return None + + return "\n".join(lines) + + def fetch_body(client: Garmin, day: str) -> str | None: """Fetch and format body/activity summary data.""" parts = [] @@ -584,6 +637,10 @@ def sync_day(client: Garmin, day: date, output_dir: Path) -> None: if sleep: sections.append(sleep) + lifestyle = fetch_lifestyle(client, day_str) + if lifestyle: + sections.append(lifestyle) + body = fetch_body(client, day_str) if body: sections.append(body) From 1496e33a03d576c262aecedb55aceb04e00c88fe Mon Sep 17 00:00:00 2001 From: Lubo Molent <23703392+lmolent@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:26:15 +0100 Subject: [PATCH 3/3] Update README with lifestyle and sleep metrics documentation --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 908c9b3..6eeb635 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ An [OpenClaw](https://openclaw.ai) skill that syncs your daily health data from ## What it syncs -- **Sleep** — duration, stages (deep/light/REM/awake), sleep score +- **Sleep** — duration, stages (deep/light/REM/awake), sleep score, sleep need, sleep factors (stress, recovery, etc.) +- **Lifestyle** — alcohol, caffeine, late meal, illness (if logged) - **Body** — steps, calories, distance, floors - **Heart** — resting HR, max HR, HRV - **Body Battery & SpO2** @@ -24,6 +25,13 @@ An [OpenClaw](https://openclaw.ai) skill that syncs your daily health data from ## Sleep: 8h 39m (Good) Deep: 1h 50m | Light: 4h 30m | REM: 2h 19m | Awake: 0h 54m Sleep Score: 85 +Sleep Need: 8h 00m +Sleep Factors: Sleep Duration: Good | Stress: Fair | Recovery: Good + +## Lifestyle +- Alcohol: 1 drink +- Caffeine: 2 cups +- Late Meal: Yes ## Body: 9,720 steps | 2,317 cal Distance: 8.0 km | Floors: 42