[![GitHub Release][releases-shield]][releases] [![GitHub Activity][commits-shield]][commits] [![License][license-shield]](LICENSE) ![Project Maintenance][maintenance-shield] [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/) [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky) # Python: Garmin Connect The Garmin Connect API library comes with two examples: - **`example.py`** - Simple getting-started example showing authentication, token storage, and basic API calls - **`demo.py`** - Comprehensive demo providing access to **130+ API methods** organized into **13 categories** for easy navigation ```bash $ ./demo.py ๐Ÿ“ Exported data will be saved to the directory: 'your_data' ๐Ÿ“„ All API responses are written to: 'response.json' Attempting to login using stored tokens from: ~/.garminconnect Successfully logged in using stored tokens! ๐Ÿ“Š Your Stats Today: 4,045 steps | 1445.0 kcal ๐ŸŒ Time to get those legs moving! ================================================== ๐Ÿšด Full-blown Garmin Connect API Demo - Main Menu ================================================== Select a category: [1] ๐Ÿ‘ค User & Profile [2] ๐Ÿ“Š Daily Health & Activity [3] ๐Ÿ”ฌ Advanced Health Metrics [4] ๐Ÿ“ˆ Historical Data & Trends [5] ๐Ÿƒ Activities & Workouts [6] โš–๏ธ Body Composition & Weight [7] ๐Ÿ† Goals & Achievements [8] โŒš Device & Technical [9] ๐ŸŽฝ Gear & Equipment [0] ๐Ÿ’ง Hydration & Wellness [a] ๐Ÿ”ง System & Export [b] ๐Ÿ“… Training Plans [c] โ›ณ Golf [d] โœ๏ธ Activity Editing [q] Exit program Make your selection: ``` ## API Coverage Statistics - **Total API Methods**: 144+ unique endpoints (snapshot) - **Categories**: 13 organized sections - **User & Profile**: 4 methods (basic user info, settings) - **Daily Health & Activity**: 10 methods (today's health data plus daily calories, resting HR and sleep ranges) - **Advanced Health Metrics**: 16 methods (fitness metrics, HRV, VO2, FTP range, training readiness, training zones, running tolerance) - **Historical Data & Trends**: 9 methods (date range queries, weekly aggregates) - **Activities & Workouts**: 41 methods (comprehensive activity, workout management, typed workout uploads including strength, in-place edit, scheduling, push to device, import, edit description / exercise sets) - **Body Composition & Weight**: 8 methods (weight tracking, body composition) - **Goals & Achievements**: 15 methods (challenges, badges, goals) - **Device & Technical**: 7 methods (device info, settings) - **Gear & Equipment**: 7 methods (gear management, tracking) - **Hydration & Wellness**: 12 methods (hydration, nutrition, blood pressure, menstrual) - **System & Export**: 5 methods (reporting, logout, GraphQL, health snapshot download) - **Training Plans**: 3 methods (plans, plan by ID, adaptive plan by ID) - **Golf**: 5 methods (scorecard summary, scorecard detail, shot data, club stats, user stats) ### Interactive Features - **Enhanced User Experience**: Categorized navigation with emoji indicators - **Smart Data Management**: Interactive weigh-in deletion with search capabilities - **Comprehensive Coverage**: All major Garmin Connect features are accessible - **Error Handling**: Robust error handling with user-friendly prompts - **Data Export**: JSON export functionality for all data types [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/) [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky) A comprehensive Python3 API wrapper for Garmin Connect, providing access to health, fitness, and device data. ## ๐Ÿ“– About This library enables developers to programmatically access Garmin Connect data including: - **Health Metrics**: Heart rate, sleep, stress, body composition, SpO2, HRV - **Activity Data**: Workouts, typed workout uploads (running, cycling, swimming, walking, hiking, strength), workout scheduling, exercises, training status, performance metrics, import-style uploads (no Strava re-export) - **Nutrition**: Daily food logs, meals, and nutrition settings - **Golf**: Scorecard summaries, scorecard details, shot-by-shot data - **Device Information**: Connected devices, settings, alarms, solar data - **Goals & Achievements**: Personal records, badges, challenges, race predictions - **Historical Data**: Trends, progress tracking, date range queries Compatible with all Garmin Connect accounts. See ## ๐Ÿ“ฆ Installation Requires Python 3.12 or later. Install from PyPI: ```bash pip install --upgrade garminconnect curl_cffi ``` ## Run demo software (recommended) Clone the repo, then: ```bash python3 -m venv .venv --copies source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install -e ".[example]" python3 ./example.py # simple getting-started example python3 ./demo.py # comprehensive demo (130+ API methods) ``` ## ๐Ÿ› ๏ธ Development This project uses [PDM](https://pdm.fming.dev/) for dependency management and task automation. > **โš ๏ธ Important**: Create a virtual environment first on externally-managed Python installs (Debian/Ubuntu) to avoid system package conflicts. ```bash python3 -m venv .venv --copies source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install pdm python3 -m pdm install --group :all pre-commit install --install-hooks # optional but recommended ``` > **Note**: Using `python -m pdm` instead of `pdm` avoids PATH issues on some > Windows setups where `pip install pdm` places the `pdm` executable outside > the directories on `PATH`. Once `pdm install` has run, subsequent `pdm run ...` > commands work normally because the venv's `Scripts/` directory is on `PATH` > while the venv is active. **Development commands:** ```bash pdm run format # Auto-format code (isort, black, ruff --fix) pdm run lint # Check code quality (isort, ruff, black, mypy) pdm run codespell # Check spelling pdm run test # Run test suite pdm run testcov # Run tests with coverage report pdm run all # Run all checks (lint + codespell + pre-commit + test) pdm run clean # Clean build artifacts and cache files pdm run build # Build package for distribution pdm run publish # Build and publish to PyPI pdm run --list # Show all available commands ``` Run `pdm run format && pdm run lint && pdm run test` before submitting PRs. ## ๐Ÿ” Authentication Authentication uses the same mobile SSO flow as the official Garmin Connect Android app. No browser is needed. **How it works:** 1. **First login**: Authenticates via `sso.garmin.com/mobile/api/login` using the Android app's client ID. If MFA is required, a callback (`prompt_mfa`) prompts for the one-time code. 2. **Token exchange**: The service ticket is exchanged for DI OAuth Bearer tokens (`access_token` + `refresh_token`) via `diauth.garmin.com`. Tokens are stored at `~/.garminconnect/garmin_tokens.json`. 3. **Auto-refresh**: Before each API request the library checks whether the DI token is about to expire and refreshes it automatically โ€” no user interaction required. **Session lifetime:** - DI tokens auto-refresh indefinitely as long as the refresh token remains valid. - A full re-login with credentials (and possibly MFA) is only needed if the refresh token itself expires or is revoked. **Token storage:** ```bash ~/.garminconnect/garmin_tokens.json # saved automatically, mode 0600 ``` The containing directory is restricted to mode `0700`. Treat the token file like a password: the refresh token can provide persistent account access. Avoid putting a Garmin password in shell history or a long-lived environment variable; prefer `getpass()` or another interactive secret prompt. **Resilient login (multi-strategy + token validation):** `login()` tries several authentication strategies in order (mobile, SSO widget, web portal โ€” each with and without TLS impersonation) and only declares success when the resulting token is actually accepted by the API. If a strategy obtains a token the API later rejects (a region/account-specific condition โ€” see [#369](https://github.com/cyberjunky/python-garminconnect/issues/369)), the library transparently falls through to the next strategy. Set `Garmin(..., verify_login=False)` to restore the legacy "first token wins" behavior. **Cached-token gotcha & self-healing:** when a `tokenstore` is supplied, `login()` loads those tokens *before* the strategy chain and short-circuits if they load โ€” so stale/poisoned cached tokens used to fail every run. The library now detects this: if cached tokens are rejected by the API, it discards them and performs a fresh credential login automatically. To force a clean slate yourself (e.g. between a failed resume and a retry), call: ```python g.logout() # clears in-memory auth + cached tokens (uses GARMINTOKENS) g.logout(tokenstore) # or pass an explicit path ``` `logout()` removes only the local `garmin_tokens.json` file and preserves its directory and unrelated files. It does **not** revoke a token that has already been issued by Garmin. Revoke account access from Garmin's account/security settings if a token may have been copied or exposed. ### What running it locally does This is an unofficial client for Garmin's web services; it does not pair with the Garmin Connect phone app or connect directly to a watch. When you call `login()`, your credentials and MFA code are sent over HTTPS to Garmin's login service. The library receives an access/refresh token pair and, when a token store is supplied, caches it locally for later sessions. Subsequent API methods send that token to Garmin and can read or change the same account data that the selected method targets. The library does not automatically download an entire account. The demo writes responses, activity downloads, and health reports only when you select those actions. Demo exports are stored under `your_data/` with owner-only directory and file permissions. Run the project in a dedicated virtual environment, read the method you plan to call, and start with read-only methods. Upload, edit, delete, schedule, hydration, and weigh-in methods can change Garmin account data. ## ๐Ÿงช Testing The default suite is credential-free and excludes live-account integration tests: ```bash pdm run test # Run all tests pdm run testcov # Run tests with coverage report ``` To explicitly run live integration tests, use a test account if possible. This can create local VCR recordings and includes methods that may mutate the account: ```bash export GARMIN_EMAIL="you@example.com" read -s GARMIN_PASSWORD && export GARMIN_PASSWORD pdm run pytest -m integration --vcr-record=once unset GARMIN_PASSWORD ``` VCR recordings are ignored by Git because Garmin responses contain sensitive health, activity, location, and account data. Do not commit them. ## ๐Ÿ“ฆ Publishing For package maintainers: **Setup PyPI credentials:** ```bash pip install twine # Edit with your preferred editor, or create via here-doc: # cat > ~/.pypirc <<'EOF' # [pypi] # username = __token__ # password = # EOF ``` ```ini [pypi] username = __token__ password = ``` Recommended: use environment variables and restrict file perms ```bash chmod 600 ~/.pypirc export TWINE_USERNAME="__token__" export TWINE_PASSWORD="" ``` **Publish new version:** ```bash pdm run publish # Build and publish to PyPI ``` **Alternative publishing steps:** ```bash pdm run build # Build package only pdm publish # Publish pre-built package ``` ## ๐Ÿค Contributing We welcome contributions! Here's how you can help: - **Report Issues**: Bug reports and feature requests via GitHub issues - **Submit PRs**: Code improvements, new features, documentation updates - **Testing**: Help test new features and report compatibility issues - **Documentation**: Improve examples, add use cases, fix typos **Before contributing:** 1. Set up your dev environment (see [Development](#๏ธ-development) above) 2. Format and lint: `pdm run format && pdm run lint` 3. Run tests: `pdm run test` 4. Follow existing code style and patterns ### Jupyter Notebook Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/docs/reference.ipynb). ### Python Code Examples ```python import os from getpass import getpass from datetime import date from garminconnect import Garmin # First run: logs in and saves tokens to ~/.garminconnect # Subsequent runs: loads saved tokens and auto-refreshes client = Garmin( os.getenv("GARMIN_EMAIL"), getpass("Garmin password: "), prompt_mfa=lambda: input("MFA code: "), ) client.login("~/.garminconnect") # Get today's stats today = date.today().isoformat() stats = client.get_stats(today) # Get heart rate data hr_data = client.get_heart_rates(today) print(f"Resting HR: {hr_data.get('restingHeartRate', 'n/a')}") ``` ### Typed Workouts (Pydantic Models) The library includes optional typed workout models for creating type-safe workout definitions: ```bash pip install garminconnect[workout] ``` ```python from garminconnect.workout import ( RunningWorkout, WorkoutSegment, create_warmup_step, create_interval_step, create_distance_interval_step, create_cooldown_step, create_repeat_group, ) # Create a structured running workout workout = RunningWorkout( workoutName="Easy Run", estimatedDurationInSecs=1800, workoutSegments=[ WorkoutSegment( segmentOrder=1, sportType={"sportTypeId": 1, "sportTypeKey": "running"}, workoutSteps=[create_warmup_step(300.0)], ) ], ) # Upload and optionally schedule it result = client.upload_running_workout(workout) client.schedule_workout(result["workoutId"], "2026-03-20") # Edit it in place - keeps its id, so any schedules pointing at it stay valid workout_data = client.get_workout_by_id(result["workoutId"]) workout_data["workoutName"] = "Easy Run (revised)" client.update_workout(result["workoutId"], workout_data) # Delete a workout or remove it from the calendar client.delete_workout(workout_id) client.unschedule_workout(scheduled_workout_id) # Push a workout to a device - defaults to the last workout / last used device client.push_workout_to_device(result["workoutId"], device_id) ``` **Available workout classes:** `RunningWorkout`, `CyclingWorkout`, `SwimmingWorkout`, `WalkingWorkout`, `HikingWorkout`, `StrengthWorkout`, `MultiSportWorkout`, `FitnessEquipmentWorkout` **Strength workouts** are rep-based. Build each exercise with `create_strength_set(category, step_order, sets, reps, rest_seconds, exercise_name="", weight_kg=None)` and identify exercises with a `category` / `exercise` pair from the bundled catalog in `garminconnect.exercises` (1,527 exercises across 47 categories, with `resolve(name)` and `find(term)` helpers): ```python from garminconnect import exercises from garminconnect.workout import StrengthWorkout, WorkoutSegment, create_strength_set lat = exercises.resolve( "Lat Pull-down" ) # {'category': 'PULL_UP', 'exercise': 'LAT_PULLDOWN'} workout = StrengthWorkout( workoutName="Upper Body", estimatedDurationInSecs=0, workoutSegments=[ WorkoutSegment( segmentOrder=1, sportType={"sportTypeId": 5, "sportTypeKey": "strength_training"}, workoutSteps=[ create_strength_set( "BENCH_PRESS", step_order=1, sets=4, reps=10, rest_seconds=120 ), create_strength_set( lat["category"], step_order=4, sets=3, reps=12, rest_seconds=90, exercise_name=lat["exercise"], ), ], ) ], ) client.upload_strength_workout(workout) ``` **Helper functions:** `create_warmup_step`, `create_interval_step`, `create_distance_interval_step`, `create_recovery_step`, `create_cooldown_step`, `create_repeat_group`, `create_strength_exercise_step`, `create_strength_rest_step`, `create_strength_set` Use `create_distance_interval_step(600.0, step_order=1)` for interval steps that should end after a distance in meters instead of after a duration. ### Golf The library supports reading Garmin Golf scorecards, shot-level data, club statistics, and golf user statistics. ```python # List recent golf scorecards summary = client.get_golf_summary(limit=10) scorecard_id = summary["scorecardSummaries"][0]["id"] # Get the full scorecard details scorecard = client.get_golf_scorecard(scorecard_id) # Get shot-by-shot data # - Omit hole_numbers to receive every hole (recommended for holes 10-18). # - Single-digit holes work as a comma- or dash-separated list, e.g. "1,2,3". # Commas are normalized to dashes because Garmin only accepts '-' as the # separator. # - Double-digit holes (10-18) cannot be fetched with a filter at all; the # API either drops them or returns an empty response. If you request holes # 10-18, the library falls back to returning all 18 holes. shots = client.get_golf_shot_data(scorecard_id) shots = client.get_golf_shot_data(scorecard_id, hole_numbers="1,2,3") # Club and player statistics club_stats = client.get_golf_club_stats() user_stats = client.get_golf_user_stats() ``` ### Additional Resources - **Simple Example**: [example.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/example.py) - Getting started guide - **Comprehensive Demo**: [demo.py](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/demo.py) - All 130+ API methods - **API Documentation**: Comprehensive method documentation in source code - **Test Cases**: Real-world usage examples in `tests/` directory ## ๐Ÿ™ Acknowledgments Special thanks to all contributors who have helped improve this project: - **Community Contributors**: Bug reports, feature requests, and code improvements - **Issue Reporters**: Helping identify and resolve compatibility issues - **Feature Developers**: Adding new API endpoints and functionality - **Documentation Authors**: Improving examples and user guides This project thrives thanks to community involvement and feedback. ## ๐Ÿ’– Support This Project If you find this library useful for your projects, please consider supporting its continued development and maintenance: ### ๐ŸŒŸ Ways to Support - **โญ Star this repository** - Help others discover the project - **๐Ÿ’ฐ Financial Support** - Contribute to development and hosting costs - **๐Ÿ› Report Issues** - Help improve stability and compatibility - **๐Ÿ“– Spread the Word** - Share with other developers ### ๐Ÿ’ณ Financial Support Options [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/) [![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky) **Why Support?** - Keeps the project actively maintained - Enables faster bug fixes and new features - Supports infrastructure costs (testing, AI, CI/CD) - Shows appreciation for hundreds of hours of development Every contribution, no matter the size, makes a difference and is greatly appreciated! ๐Ÿ™ [releases-shield]: https://img.shields.io/github/release/cyberjunky/python-garminconnect.svg?style=for-the-badge [releases]: https://github.com/cyberjunky/python-garminconnect/releases [commits-shield]: https://img.shields.io/github/commit-activity/y/cyberjunky/python-garminconnect.svg?style=for-the-badge [commits]: https://github.com/cyberjunky/python-garminconnect/commits/master [license-shield]: https://img.shields.io/github/license/cyberjunky/python-garminconnect.svg?style=for-the-badge [maintenance-shield]: https://img.shields.io/badge/maintainer-cyberjunky-blue.svg?style=for-the-badge