# Directory Structure ``` ├── .env_template ├── .gitignore ├── example.py ├── garmin_mcp_server.py ├── pyproject.toml ├── README.md └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.env_template: -------------------------------------------------------------------------------- ``` 1 | GARMIN_EMAIL='[email protected]' 2 | GARMIN_PASSWORD='password' 3 | GARMIN_TOKEN_STORE='~/.garminconnect' 4 | GARMIN_TOKEN_STORE_BASE64='~/.garminconnect_base64' 5 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .env 12 | .specstory/ 13 | 14 | # Version management 15 | .python-version 16 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Garmin Connect MCP Server 2 | 3 | This project provides a server for interacting with the Garmin Connect API. It allows users to manage their Garmin data, including workouts, health metrics, and more. 4 | 5 | ## Getting Started 6 | 7 | ### Prerequisites 8 | 9 | - Python 3.x 10 | - Required Python packages (install via `uv sync`) 11 | - A Garmin Connect account 12 | - Uses Python Garmin Connect Package to interact with Garmin Connect API: https://github.com/cyberjunky/python-garminconnect 13 | 14 | ### Environment Variables 15 | 16 | Create a `.env` file in the root directory from the `.env_template` file with the following variables: 17 | 18 | - `GARMIN_EMAIL` 19 | - `GARMIN_PASSWORD` 20 | 21 | ## Generate token for Garmin Connect 22 | 23 | ```bash 24 | python example.py 25 | ``` 26 | 27 | ## Use MCP Inspector 28 | 29 | ```bash 30 | mcp dev garmin_mcp_server.py 31 | ``` 32 | 33 | ## Register MCP Server in Claude Desktop 34 | 35 | ```bash 36 | mcp install garmin_mcp_server.py 37 | ``` 38 | 39 | ### Running the Server 40 | 41 | 42 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "garmin-mcp" 3 | version = "0.1.0" 4 | description = "Garmin Connect MCP Server" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "garminconnect>=0.2.25", 9 | "mcp[cli]>=1.3.0", 10 | "readchar>=4.2.1", 11 | ] 12 | ``` -------------------------------------------------------------------------------- /garmin_mcp_server.py: -------------------------------------------------------------------------------- ```python 1 | # server.py 2 | import datetime 3 | from mcp.server.fastmcp import FastMCP 4 | import logging 5 | 6 | import requests 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | from dotenv import load_dotenv 11 | import os 12 | from garth.exc import GarthHTTPError 13 | 14 | from garminconnect import ( 15 | Garmin, 16 | GarminConnectAuthenticationError, 17 | GarminConnectConnectionError, 18 | GarminConnectTooManyRequestsError, 19 | ) 20 | 21 | # Load environment variables from .env file 22 | load_dotenv() 23 | 24 | print(f"Starting Garmin MCP server for {os.getenv('GARMIN_EMAIL')}") 25 | 26 | # Load environment variables if defined 27 | email = os.getenv("GARMIN_EMAIL") 28 | password = os.getenv("GARMIN_PASSWORD") 29 | tokenstore = os.getenv("GARMIN_TOKEN_STORE") or "~/.garminconnect" 30 | tokenstore_base64 = os.getenv("GARMINTOKENS_BASE64") or "~/.garminconnect_base64" 31 | api = None 32 | 33 | def init_api(email, password): 34 | """Initialize Garmin API with your credentials.""" 35 | 36 | try: 37 | # Using Oauth1 and OAuth2 token files from directory 38 | print( 39 | f"Trying to login to Garmin Connect using token data from directory '{tokenstore}'...\n" 40 | ) 41 | 42 | # Using Oauth1 and Oauth2 tokens from base64 encoded string 43 | # print( 44 | # f"Trying to login to Garmin Connect using token data from file '{tokenstore_base64}'...\n" 45 | # ) 46 | # dir_path = os.path.expanduser(tokenstore_base64) 47 | # with open(dir_path, "r") as token_file: 48 | # tokenstore = token_file.read() 49 | 50 | garmin = Garmin() 51 | garmin.login(tokenstore) 52 | 53 | except (FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError): 54 | # Session is expired. You'll need to log in again 55 | print( 56 | "Login tokens not present, login with your Garmin Connect credentials to generate them.\n" 57 | f"They will be stored in '{tokenstore}' for future use.\n" 58 | ) 59 | try: 60 | # # Ask for credentials if not set as environment variables 61 | # if not email or not password: 62 | # email, password = get_credentials() 63 | 64 | garmin = Garmin( 65 | email=email, password=password, is_cn=False, prompt_mfa=get_mfa 66 | ) 67 | garmin.login() 68 | # Save Oauth1 and Oauth2 token files to directory for next login 69 | garmin.garth.dump(tokenstore) 70 | print( 71 | f"Oauth tokens stored in '{tokenstore}' directory for future use. (first method)\n" 72 | ) 73 | # Encode Oauth1 and Oauth2 tokens to base64 string and safe to file for next login (alternative way) 74 | token_base64 = garmin.garth.dumps() 75 | dir_path = os.path.expanduser(tokenstore_base64) 76 | with open(dir_path, "w") as token_file: 77 | token_file.write(token_base64) 78 | print( 79 | f"Oauth tokens encoded as base64 string and saved to '{dir_path}' file for future use. (second method)\n" 80 | ) 81 | except ( 82 | FileNotFoundError, 83 | GarthHTTPError, 84 | GarminConnectAuthenticationError, 85 | requests.exceptions.HTTPError, 86 | ) as err: 87 | logger.error(err) 88 | return None 89 | 90 | return garmin 91 | 92 | 93 | 94 | api = init_api(email, password) 95 | 96 | # Create an MCP server 97 | mcp = FastMCP("Garmin Connect MCP Server") 98 | 99 | # Add an addition tool 100 | @mcp.tool() 101 | def fetch_sleep_data(date: str) -> dict: 102 | """Returns sleep data for a given date 103 | Args: 104 | date: str - date in format YYYY-MM-DD 105 | Returns: 106 | dict - sleep data 107 | """ 108 | return api.get_sleep_data(date) 109 | 110 | @mcp.tool() 111 | def fetch_steps_data(date_from: str, date_to: str) -> dict: 112 | """Returns steps data for the for the given date range 113 | Args: 114 | date_from: str - start date in format YYYY-MM-DD 115 | date_to: str - end date in format YYYY-MM-DD 116 | Returns: 117 | dict - steps data 118 | """ 119 | return api.get_daily_steps(date_from, date_to) 120 | 121 | @mcp.tool() 122 | def fetch_activities_data(num_activities: int) -> dict: 123 | """Returns activitie data for the for the given date range 124 | Args: 125 | num_activitie: int - number of activitie to fetch 126 | Returns: 127 | dict - workouts data 128 | """ 129 | activities = api.get_activities(limit=num_activities) 130 | 131 | # # Get last fetched workout 132 | # workout_id = workouts[-1]["workoutId"] 133 | # workout_name = workouts[-1]["workoutName"] 134 | 135 | # downloaded_workouts = [] 136 | # for i in range(num_workouts): 137 | # workout_id = workouts[-(i + 1)]["workoutId"] 138 | # workout_name = workouts[-(i + 1)]["workoutName"] 139 | 140 | # workout_data = api.download_workout(workout_id) 141 | 142 | # downloaded_workouts.append(workout_data) 143 | 144 | # return downloaded_workouts 145 | return activities 146 | 147 | @mcp.tool() 148 | def fetch_heart_rate_data(date: str) -> dict: 149 | """Returns heart rate data for a given date 150 | Args: 151 | date: str - date in format YYYY-MM-DD 152 | Returns: 153 | dict - heart rate data 154 | """ 155 | return api.get_rhr_day(date) 156 | 157 | @mcp.tool() 158 | def fetch_stress_data(date: str) -> dict: 159 | """Returns stress data for a given date 160 | Args: 161 | date: str - date in format YYYY-MM-DD 162 | Returns: 163 | dict - stress data 164 | """ 165 | return api.get_stress_data(date) 166 | 167 | @mcp.tool() 168 | def fetch_body_battery_data(start_date: str, end_date: str) -> dict: 169 | """Returns body battery data for a given date 170 | Args: 171 | start_date: str - start date in format YYYY-MM-DD 172 | end_date: str - end date in format YYYY-MM-DD 173 | Returns: 174 | dict - body battery data 175 | """ 176 | api.get_body_battery(start_date, end_date) 177 | 178 | 179 | 180 | # Add a dynamic greeting resource 181 | @mcp.resource("greeting://{name}") 182 | def get_greeting(name: str) -> str: 183 | """Get a personalized greeting""" 184 | return f"Hello, {name}!" 185 | 186 | 187 | ``` -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | pip3 install garth requests readchar 4 | 5 | export EMAIL=<your garmin email> 6 | export PASSWORD=<your garmin password> 7 | 8 | """ 9 | import datetime 10 | from datetime import timezone 11 | import json 12 | import logging 13 | import os 14 | import sys 15 | from getpass import getpass 16 | 17 | 18 | import readchar 19 | import requests 20 | from garth.exc import GarthHTTPError 21 | 22 | from garminconnect import ( 23 | Garmin, 24 | GarminConnectAuthenticationError, 25 | GarminConnectConnectionError, 26 | GarminConnectTooManyRequestsError, 27 | ) 28 | 29 | # Configure debug logging 30 | # logging.basicConfig(level=logging.DEBUG) 31 | logging.basicConfig(level=logging.INFO) 32 | logger = logging.getLogger(__name__) 33 | 34 | from dotenv import load_dotenv 35 | 36 | # Load environment variables from .env file 37 | load_dotenv() 38 | 39 | 40 | # Load environment variables if defined 41 | email = os.getenv("GARMIN_EMAIL") 42 | password = os.getenv("GARMIN_PASSWORD") 43 | tokenstore = os.getenv("GARMIN_TOKEN_STORE") or "~/.garminconnect" 44 | tokenstore_base64 = os.getenv("GARMINTOKENS_BASE64") or "~/.garminconnect_base64" 45 | api = None 46 | 47 | # Example selections and settings 48 | 49 | # Let's say we want to scrape all activities using switch menu_option "p". We change the values of the below variables, IE startdate days, limit,... 50 | today = datetime.date.today() 51 | startdate = today - datetime.timedelta(days=7) # Select past week 52 | start = 0 53 | limit = 100 54 | start_badge = 1 # Badge related calls calls start counting at 1 55 | activitytype = "" # Possible values are: cycling, running, swimming, multi_sport, fitness_equipment, hiking, walking, other 56 | activityfile = "MY_ACTIVITY.fit" # Supported file types are: .fit .gpx .tcx 57 | weight = 89.6 58 | weightunit = "kg" 59 | # workout_example = """ 60 | # { 61 | # 'workoutId': "random_id", 62 | # 'ownerId': "random", 63 | # 'workoutName': 'Any workout name', 64 | # 'description': 'FTP 200, TSS 1, NP 114, IF 0.57', 65 | # 'sportType': {'sportTypeId': 2, 'sportTypeKey': 'cycling'}, 66 | # 'workoutSegments': [ 67 | # { 68 | # 'segmentOrder': 1, 69 | # 'sportType': {'sportTypeId': 2, 'sportTypeKey': 'cycling'}, 70 | # 'workoutSteps': [ 71 | # {'type': 'ExecutableStepDTO', 'stepOrder': 1, 72 | # 'stepType': {'stepTypeId': 3, 'stepTypeKey': 'interval'}, 'childStepId': None, 73 | # 'endCondition': {'conditionTypeId': 2, 'conditionTypeKey': 'time'}, 'endConditionValue': 60, 74 | # 'targetType': {'workoutTargetTypeId': 2, 'workoutTargetTypeKey': 'power.zone'}, 75 | # 'targetValueOne': 95, 'targetValueTwo': 105}, 76 | # {'type': 'ExecutableStepDTO', 'stepOrder': 2, 77 | # 'stepType': {'stepTypeId': 3, 'stepTypeKey': 'interval'}, 'childStepId': None, 78 | # 'endCondition': {'conditionTypeId': 2, 'conditionTypeKey': 'time'}, 'endConditionValue': 120, 79 | # 'targetType': {'workoutTargetTypeId': 2, 'workoutTargetTypeKey': 'power.zone'}, 80 | # 'targetValueOne': 114, 'targetValueTwo': 126} 81 | # ] 82 | # } 83 | # ] 84 | # } 85 | # """ 86 | 87 | menu_options = { 88 | "1": "Get full name", 89 | "2": "Get unit system", 90 | "3": f"Get activity data for '{today.isoformat()}'", 91 | "4": f"Get activity data for '{today.isoformat()}' (compatible with garminconnect-ha)", 92 | "5": f"Get body composition data for '{today.isoformat()}' (compatible with garminconnect-ha)", 93 | "6": f"Get body composition data for from '{startdate.isoformat()}' to '{today.isoformat()}' (to be compatible with garminconnect-ha)", 94 | "7": f"Get stats and body composition data for '{today.isoformat()}'", 95 | "8": f"Get steps data for '{today.isoformat()}'", 96 | "9": f"Get heart rate data for '{today.isoformat()}'", 97 | "0": f"Get training readiness data for '{today.isoformat()}'", 98 | "-": f"Get daily step data for '{startdate.isoformat()}' to '{today.isoformat()}'", 99 | "/": f"Get body battery data for '{startdate.isoformat()}' to '{today.isoformat()}'", 100 | "!": f"Get floors data for '{startdate.isoformat()}'", 101 | "?": f"Get blood pressure data for '{startdate.isoformat()}' to '{today.isoformat()}'", 102 | ".": f"Get training status data for '{today.isoformat()}'", 103 | "a": f"Get resting heart rate data for {today.isoformat()}'", 104 | "b": f"Get hydration data for '{today.isoformat()}'", 105 | "c": f"Get sleep data for '{today.isoformat()}'", 106 | "d": f"Get stress data for '{today.isoformat()}'", 107 | "e": f"Get respiration data for '{today.isoformat()}'", 108 | "f": f"Get SpO2 data for '{today.isoformat()}'", 109 | "g": f"Get max metric data (like vo2MaxValue and fitnessAge) for '{today.isoformat()}'", 110 | "h": "Get personal record for user", 111 | "i": "Get earned badges for user", 112 | "j": f"Get adhoc challenges data from start '{start}' and limit '{limit}'", 113 | "k": f"Get available badge challenges data from '{start_badge}' and limit '{limit}'", 114 | "l": f"Get badge challenges data from '{start_badge}' and limit '{limit}'", 115 | "m": f"Get non completed badge challenges data from '{start_badge}' and limit '{limit}'", 116 | "n": f"Get activities data from start '{start}' and limit '{limit}'", 117 | "o": "Get last activity", 118 | "p": f"Download activities data by date from '{startdate.isoformat()}' to '{today.isoformat()}'", 119 | "r": f"Get all kinds of activities data from '{start}'", 120 | "s": f"Upload activity data from file '{activityfile}'", 121 | "t": "Get all kinds of Garmin device info", 122 | "u": "Get active goals", 123 | "v": "Get future goals", 124 | "w": "Get past goals", 125 | "y": "Get all Garmin device alarms", 126 | "x": f"Get Heart Rate Variability data (HRV) for '{today.isoformat()}'", 127 | "z": f"Get progress summary from '{startdate.isoformat()}' to '{today.isoformat()}' for all metrics", 128 | "A": "Get gear, the defaults, activity types and statistics", 129 | "B": f"Get weight-ins from '{startdate.isoformat()}' to '{today.isoformat()}'", 130 | "C": f"Get daily weigh-ins for '{today.isoformat()}'", 131 | "D": f"Delete all weigh-ins for '{today.isoformat()}'", 132 | "E": f"Add a weigh-in of {weight}{weightunit} on '{today.isoformat()}'", 133 | "F": f"Get virtual challenges/expeditions from '{startdate.isoformat()}' to '{today.isoformat()}'", 134 | "G": f"Get hill score data from '{startdate.isoformat()}' to '{today.isoformat()}'", 135 | "H": f"Get endurance score data from '{startdate.isoformat()}' to '{today.isoformat()}'", 136 | "I": f"Get activities for date '{today.isoformat()}'", 137 | "J": "Get race predictions", 138 | "K": f"Get all day stress data for '{today.isoformat()}'", 139 | "L": f"Add body composition for '{today.isoformat()}'", 140 | "M": "Set blood pressure '120,80,80,notes='Testing with example.py'", 141 | "N": "Get user profile/settings", 142 | "O": f"Reload epoch data for {today.isoformat()}", 143 | "P": "Get workouts 0-100, get and download last one to .FIT file", 144 | # "Q": "Upload workout from json data", 145 | "R": "Get solar data from your devices", 146 | "S": "Get pregnancy summary data", 147 | "T": "Add hydration data", 148 | "U": f"Get Fitness Age data for {today.isoformat()}", 149 | "V": f"Get daily wellness events data for {startdate.isoformat()}", 150 | "W": "Get userprofile settings", 151 | "Z": "Remove stored login tokens (logout)", 152 | "q": "Exit", 153 | } 154 | 155 | 156 | def display_json(api_call, output): 157 | """Format API output for better readability.""" 158 | 159 | dashed = "-" * 20 160 | header = f"{dashed} {api_call} {dashed}" 161 | footer = "-" * len(header) 162 | 163 | print(header) 164 | 165 | if isinstance(output, (int, str, dict, list)): 166 | print(json.dumps(output, indent=4)) 167 | else: 168 | print(output) 169 | 170 | print(footer) 171 | 172 | 173 | def display_text(output): 174 | """Format API output for better readability.""" 175 | 176 | dashed = "-" * 60 177 | header = f"{dashed}" 178 | footer = "-" * len(header) 179 | 180 | print(header) 181 | print(json.dumps(output, indent=4)) 182 | print(footer) 183 | 184 | 185 | def get_credentials(): 186 | """Get user credentials.""" 187 | 188 | email = input("Login e-mail: ") 189 | password = getpass("Enter password: ") 190 | 191 | return email, password 192 | 193 | 194 | def init_api(email, password): 195 | """Initialize Garmin API with your credentials.""" 196 | 197 | try: 198 | # Using Oauth1 and OAuth2 token files from directory 199 | print( 200 | f"Trying to login to Garmin Connect using token data from directory '{tokenstore}'...\n" 201 | ) 202 | 203 | # Using Oauth1 and Oauth2 tokens from base64 encoded string 204 | # print( 205 | # f"Trying to login to Garmin Connect using token data from file '{tokenstore_base64}'...\n" 206 | # ) 207 | # dir_path = os.path.expanduser(tokenstore_base64) 208 | # with open(dir_path, "r") as token_file: 209 | # tokenstore = token_file.read() 210 | 211 | garmin = Garmin() 212 | garmin.login(tokenstore) 213 | 214 | except (FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError): 215 | # Session is expired. You'll need to log in again 216 | print( 217 | "Login tokens not present, login with your Garmin Connect credentials to generate them.\n" 218 | f"They will be stored in '{tokenstore}' for future use.\n" 219 | ) 220 | try: 221 | # Ask for credentials if not set as environment variables 222 | if not email or not password: 223 | email, password = get_credentials() 224 | 225 | garmin = Garmin( 226 | email=email, password=password, is_cn=False, prompt_mfa=get_mfa 227 | ) 228 | garmin.login() 229 | # Save Oauth1 and Oauth2 token files to directory for next login 230 | garmin.garth.dump(tokenstore) 231 | print( 232 | f"Oauth tokens stored in '{tokenstore}' directory for future use. (first method)\n" 233 | ) 234 | # Encode Oauth1 and Oauth2 tokens to base64 string and safe to file for next login (alternative way) 235 | token_base64 = garmin.garth.dumps() 236 | dir_path = os.path.expanduser(tokenstore_base64) 237 | with open(dir_path, "w") as token_file: 238 | token_file.write(token_base64) 239 | print( 240 | f"Oauth tokens encoded as base64 string and saved to '{dir_path}' file for future use. (second method)\n" 241 | ) 242 | except ( 243 | FileNotFoundError, 244 | GarthHTTPError, 245 | GarminConnectAuthenticationError, 246 | requests.exceptions.HTTPError, 247 | ) as err: 248 | logger.error(err) 249 | return None 250 | 251 | return garmin 252 | 253 | 254 | def get_mfa(): 255 | """Get MFA.""" 256 | 257 | return input("MFA one-time code: ") 258 | 259 | 260 | def print_menu(): 261 | """Print examples menu.""" 262 | for key in menu_options.keys(): 263 | print(f"{key} -- {menu_options[key]}") 264 | print("Make your selection: ", end="", flush=True) 265 | 266 | 267 | def switch(api, i): 268 | """Run selected API call.""" 269 | 270 | # Exit example program 271 | if i == "q": 272 | print("Be active, generate some data to fetch next time ;-) Bye!") 273 | sys.exit() 274 | 275 | # Skip requests if login failed 276 | if api: 277 | try: 278 | print(f"\n\nExecuting: {menu_options[i]}\n") 279 | 280 | # USER BASICS 281 | if i == "1": 282 | # Get full name from profile 283 | display_json("api.get_full_name()", api.get_full_name()) 284 | elif i == "2": 285 | # Get unit system from profile 286 | display_json("api.get_unit_system()", api.get_unit_system()) 287 | 288 | # USER STATISTIC SUMMARIES 289 | elif i == "3": 290 | # Get activity data for 'YYYY-MM-DD' 291 | display_json( 292 | f"api.get_stats('{today.isoformat()}')", 293 | api.get_stats(today.isoformat()), 294 | ) 295 | elif i == "4": 296 | # Get activity data (to be compatible with garminconnect-ha) 297 | display_json( 298 | f"api.get_user_summary('{today.isoformat()}')", 299 | api.get_user_summary(today.isoformat()), 300 | ) 301 | elif i == "5": 302 | # Get body composition data for 'YYYY-MM-DD' (to be compatible with garminconnect-ha) 303 | display_json( 304 | f"api.get_body_composition('{today.isoformat()}')", 305 | api.get_body_composition(today.isoformat()), 306 | ) 307 | elif i == "6": 308 | # Get body composition data for multiple days 'YYYY-MM-DD' (to be compatible with garminconnect-ha) 309 | display_json( 310 | f"api.get_body_composition('{startdate.isoformat()}', '{today.isoformat()}')", 311 | api.get_body_composition(startdate.isoformat(), today.isoformat()), 312 | ) 313 | elif i == "7": 314 | # Get stats and body composition data for 'YYYY-MM-DD' 315 | display_json( 316 | f"api.get_stats_and_body('{today.isoformat()}')", 317 | api.get_stats_and_body(today.isoformat()), 318 | ) 319 | 320 | # USER STATISTICS LOGGED 321 | elif i == "8": 322 | # Get steps data for 'YYYY-MM-DD' 323 | display_json( 324 | f"api.get_steps_data('{today.isoformat()}')", 325 | api.get_steps_data(today.isoformat()), 326 | ) 327 | elif i == "9": 328 | # Get heart rate data for 'YYYY-MM-DD' 329 | display_json( 330 | f"api.get_heart_rates('{today.isoformat()}')", 331 | api.get_heart_rates(today.isoformat()), 332 | ) 333 | elif i == "0": 334 | # Get training readiness data for 'YYYY-MM-DD' 335 | display_json( 336 | f"api.get_training_readiness('{today.isoformat()}')", 337 | api.get_training_readiness(today.isoformat()), 338 | ) 339 | elif i == "/": 340 | # Get daily body battery data for 'YYYY-MM-DD' to 'YYYY-MM-DD' 341 | display_json( 342 | f"api.get_body_battery('{startdate.isoformat()}, {today.isoformat()}')", 343 | api.get_body_battery(startdate.isoformat(), today.isoformat()), 344 | ) 345 | # Get daily body battery event data for 'YYYY-MM-DD' 346 | display_json( 347 | f"api.get_body_battery_events('{startdate.isoformat()}, {today.isoformat()}')", 348 | api.get_body_battery_events(startdate.isoformat()), 349 | ) 350 | elif i == "?": 351 | # Get daily blood pressure data for 'YYYY-MM-DD' to 'YYYY-MM-DD' 352 | display_json( 353 | f"api.get_blood_pressure('{startdate.isoformat()}, {today.isoformat()}')", 354 | api.get_blood_pressure(startdate.isoformat(), today.isoformat()), 355 | ) 356 | elif i == "-": 357 | # Get daily step data for 'YYYY-MM-DD' 358 | display_json( 359 | f"api.get_daily_steps('{startdate.isoformat()}, {today.isoformat()}')", 360 | api.get_daily_steps(startdate.isoformat(), today.isoformat()), 361 | ) 362 | elif i == "!": 363 | # Get daily floors data for 'YYYY-MM-DD' 364 | display_json( 365 | f"api.get_floors('{today.isoformat()}')", 366 | api.get_floors(today.isoformat()), 367 | ) 368 | elif i == ".": 369 | # Get training status data for 'YYYY-MM-DD' 370 | display_json( 371 | f"api.get_training_status('{today.isoformat()}')", 372 | api.get_training_status(today.isoformat()), 373 | ) 374 | elif i == "a": 375 | # Get resting heart rate data for 'YYYY-MM-DD' 376 | display_json( 377 | f"api.get_rhr_day('{today.isoformat()}')", 378 | api.get_rhr_day(today.isoformat()), 379 | ) 380 | elif i == "b": 381 | # Get hydration data 'YYYY-MM-DD' 382 | display_json( 383 | f"api.get_hydration_data('{today.isoformat()}')", 384 | api.get_hydration_data(today.isoformat()), 385 | ) 386 | elif i == "c": 387 | # Get sleep data for 'YYYY-MM-DD' 388 | display_json( 389 | f"api.get_sleep_data('{today.isoformat()}')", 390 | api.get_sleep_data(today.isoformat()), 391 | ) 392 | elif i == "d": 393 | # Get stress data for 'YYYY-MM-DD' 394 | display_json( 395 | f"api.get_stress_data('{today.isoformat()}')", 396 | api.get_stress_data(today.isoformat()), 397 | ) 398 | elif i == "e": 399 | # Get respiration data for 'YYYY-MM-DD' 400 | display_json( 401 | f"api.get_respiration_data('{today.isoformat()}')", 402 | api.get_respiration_data(today.isoformat()), 403 | ) 404 | elif i == "f": 405 | # Get SpO2 data for 'YYYY-MM-DD' 406 | display_json( 407 | f"api.get_spo2_data('{today.isoformat()}')", 408 | api.get_spo2_data(today.isoformat()), 409 | ) 410 | elif i == "g": 411 | # Get max metric data (like vo2MaxValue and fitnessAge) for 'YYYY-MM-DD' 412 | display_json( 413 | f"api.get_max_metrics('{today.isoformat()}')", 414 | api.get_max_metrics(today.isoformat()), 415 | ) 416 | elif i == "h": 417 | # Get personal record for user 418 | display_json("api.get_personal_record()", api.get_personal_record()) 419 | elif i == "i": 420 | # Get earned badges for user 421 | display_json("api.get_earned_badges()", api.get_earned_badges()) 422 | elif i == "j": 423 | # Get adhoc challenges data from start and limit 424 | display_json( 425 | f"api.get_adhoc_challenges({start},{limit})", 426 | api.get_adhoc_challenges(start, limit), 427 | ) # 1=start, 100=limit 428 | elif i == "k": 429 | # Get available badge challenges data from start and limit 430 | display_json( 431 | f"api.get_available_badge_challenges({start_badge}, {limit})", 432 | api.get_available_badge_challenges(start_badge, limit), 433 | ) # 1=start, 100=limit 434 | elif i == "l": 435 | # Get badge challenges data from start and limit 436 | display_json( 437 | f"api.get_badge_challenges({start_badge}, {limit})", 438 | api.get_badge_challenges(start_badge, limit), 439 | ) # 1=start, 100=limit 440 | elif i == "m": 441 | # Get non completed badge challenges data from start and limit 442 | display_json( 443 | f"api.get_non_completed_badge_challenges({start_badge}, {limit})", 444 | api.get_non_completed_badge_challenges(start_badge, limit), 445 | ) # 1=start, 100=limit 446 | 447 | # ACTIVITIES 448 | elif i == "n": 449 | # Get activities data from start and limit 450 | display_json( 451 | f"api.get_activities({start}, {limit})", 452 | api.get_activities(start, limit), 453 | ) # 0=start, 1=limit 454 | elif i == "o": 455 | # Get last activity 456 | display_json("api.get_last_activity()", api.get_last_activity()) 457 | elif i == "p": 458 | # Get activities data from startdate 'YYYY-MM-DD' to enddate 'YYYY-MM-DD', with (optional) activitytype 459 | # Possible values are: cycling, running, swimming, multi_sport, fitness_equipment, hiking, walking, other 460 | activities = api.get_activities_by_date( 461 | startdate.isoformat(), today.isoformat(), activitytype 462 | ) 463 | 464 | # Download activities 465 | for activity in activities: 466 | activity_start_time = datetime.datetime.strptime( 467 | activity["startTimeLocal"], "%Y-%m-%d %H:%M:%S" 468 | ).strftime( 469 | "%d-%m-%Y" 470 | ) # Format as DD-MM-YYYY, for creating unique activity names for scraping 471 | activity_id = activity["activityId"] 472 | activity_name = activity["activityName"] 473 | display_text(activity) 474 | 475 | print( 476 | f"api.download_activity({activity_id}, dl_fmt=api.ActivityDownloadFormat.GPX)" 477 | ) 478 | gpx_data = api.download_activity( 479 | activity_id, dl_fmt=api.ActivityDownloadFormat.GPX 480 | ) 481 | output_file = f"./{str(activity_name)}_{str(activity_start_time)}_{str(activity_id)}.gpx" 482 | with open(output_file, "wb") as fb: 483 | fb.write(gpx_data) 484 | print(f"Activity data downloaded to file {output_file}") 485 | 486 | print( 487 | f"api.download_activity({activity_id}, dl_fmt=api.ActivityDownloadFormat.TCX)" 488 | ) 489 | tcx_data = api.download_activity( 490 | activity_id, dl_fmt=api.ActivityDownloadFormat.TCX 491 | ) 492 | output_file = f"./{str(activity_name)}_{str(activity_start_time)}_{str(activity_id)}.tcx" 493 | with open(output_file, "wb") as fb: 494 | fb.write(tcx_data) 495 | print(f"Activity data downloaded to file {output_file}") 496 | 497 | print( 498 | f"api.download_activity({activity_id}, dl_fmt=api.ActivityDownloadFormat.ORIGINAL)" 499 | ) 500 | zip_data = api.download_activity( 501 | activity_id, dl_fmt=api.ActivityDownloadFormat.ORIGINAL 502 | ) 503 | output_file = f"./{str(activity_name)}_{str(activity_start_time)}_{str(activity_id)}.zip" 504 | with open(output_file, "wb") as fb: 505 | fb.write(zip_data) 506 | print(f"Activity data downloaded to file {output_file}") 507 | 508 | print( 509 | f"api.download_activity({activity_id}, dl_fmt=api.ActivityDownloadFormat.CSV)" 510 | ) 511 | csv_data = api.download_activity( 512 | activity_id, dl_fmt=api.ActivityDownloadFormat.CSV 513 | ) 514 | output_file = f"./{str(activity_name)}_{str(activity_start_time)}_{str(activity_id)}.csv" 515 | with open(output_file, "wb") as fb: 516 | fb.write(csv_data) 517 | print(f"Activity data downloaded to file {output_file}") 518 | 519 | elif i == "r": 520 | # Get activities data from start and limit 521 | activities = api.get_activities(start, limit) # 0=start, 1=limit 522 | 523 | # Get activity splits 524 | first_activity_id = activities[0].get("activityId") 525 | 526 | display_json( 527 | f"api.get_activity_splits({first_activity_id})", 528 | api.get_activity_splits(first_activity_id), 529 | ) 530 | 531 | # Get activity typed splits 532 | 533 | display_json( 534 | f"api.get_activity_typed_splits({first_activity_id})", 535 | api.get_activity_typed_splits(first_activity_id), 536 | ) 537 | # Get activity split summaries for activity id 538 | display_json( 539 | f"api.get_activity_split_summaries({first_activity_id})", 540 | api.get_activity_split_summaries(first_activity_id), 541 | ) 542 | 543 | # Get activity weather data for activity 544 | display_json( 545 | f"api.get_activity_weather({first_activity_id})", 546 | api.get_activity_weather(first_activity_id), 547 | ) 548 | 549 | # Get activity hr timezones id 550 | display_json( 551 | f"api.get_activity_hr_in_timezones({first_activity_id})", 552 | api.get_activity_hr_in_timezones(first_activity_id), 553 | ) 554 | 555 | # Get activity details for activity id 556 | display_json( 557 | f"api.get_activity_details({first_activity_id})", 558 | api.get_activity_details(first_activity_id), 559 | ) 560 | 561 | # Get gear data for activity id 562 | display_json( 563 | f"api.get_activity_gear({first_activity_id})", 564 | api.get_activity_gear(first_activity_id), 565 | ) 566 | 567 | # Activity data for activity id 568 | display_json( 569 | f"api.get_activity({first_activity_id})", 570 | api.get_activity(first_activity_id), 571 | ) 572 | 573 | # Get exercise sets in case the activity is a strength_training 574 | if activities[0]["activityType"]["typeKey"] == "strength_training": 575 | display_json( 576 | f"api.get_activity_exercise_sets({first_activity_id})", 577 | api.get_activity_exercise_sets(first_activity_id), 578 | ) 579 | 580 | elif i == "s": 581 | try: 582 | # Upload activity from file 583 | display_json( 584 | f"api.upload_activity({activityfile})", 585 | api.upload_activity(activityfile), 586 | ) 587 | except FileNotFoundError: 588 | print(f"File to upload not found: {activityfile}") 589 | 590 | # DEVICES 591 | elif i == "t": 592 | # Get Garmin devices 593 | devices = api.get_devices() 594 | display_json("api.get_devices()", devices) 595 | 596 | # Get device last used 597 | device_last_used = api.get_device_last_used() 598 | display_json("api.get_device_last_used()", device_last_used) 599 | 600 | # Get settings per device 601 | for device in devices: 602 | device_id = device["deviceId"] 603 | display_json( 604 | f"api.get_device_settings({device_id})", 605 | api.get_device_settings(device_id), 606 | ) 607 | 608 | # Get primary training device information 609 | primary_training_device = api.get_primary_training_device() 610 | display_json( 611 | "api.get_primary_training_device()", primary_training_device 612 | ) 613 | 614 | elif i == "R": 615 | # Get solar data from Garmin devices 616 | devices = api.get_devices() 617 | display_json("api.get_devices()", devices) 618 | 619 | # Get device last used 620 | device_last_used = api.get_device_last_used() 621 | display_json("api.get_device_last_used()", device_last_used) 622 | 623 | # Get settings per device 624 | for device in devices: 625 | device_id = device["deviceId"] 626 | display_json( 627 | f"api.get_device_solar_data({device_id}, {today.isoformat()})", 628 | api.get_device_solar_data(device_id, today.isoformat()), 629 | ) 630 | # GOALS 631 | elif i == "u": 632 | # Get active goals 633 | goals = api.get_goals("active") 634 | display_json('api.get_goals("active")', goals) 635 | 636 | elif i == "v": 637 | # Get future goals 638 | goals = api.get_goals("future") 639 | display_json('api.get_goals("future")', goals) 640 | 641 | elif i == "w": 642 | # Get past goals 643 | goals = api.get_goals("past") 644 | display_json('api.get_goals("past")', goals) 645 | 646 | # ALARMS 647 | elif i == "y": 648 | # Get Garmin device alarms 649 | alarms = api.get_device_alarms() 650 | for alarm in alarms: 651 | alarm_id = alarm["alarmId"] 652 | display_json(f"api.get_device_alarms({alarm_id})", alarm) 653 | 654 | elif i == "x": 655 | # Get Heart Rate Variability (hrv) data 656 | display_json( 657 | f"api.get_hrv_data({today.isoformat()})", 658 | api.get_hrv_data(today.isoformat()), 659 | ) 660 | 661 | elif i == "z": 662 | # Get progress summary 663 | for metric in [ 664 | "elevationGain", 665 | "duration", 666 | "distance", 667 | "movingDuration", 668 | ]: 669 | display_json( 670 | f"api.get_progress_summary_between_dates({today.isoformat()})", 671 | api.get_progress_summary_between_dates( 672 | startdate.isoformat(), today.isoformat(), metric 673 | ), 674 | ) 675 | # GEAR 676 | elif i == "A": 677 | last_used_device = api.get_device_last_used() 678 | display_json("api.get_device_last_used()", last_used_device) 679 | userProfileNumber = last_used_device["userProfileNumber"] 680 | gear = api.get_gear(userProfileNumber) 681 | display_json("api.get_gear()", gear) 682 | display_json( 683 | "api.get_gear_defaults()", api.get_gear_defaults(userProfileNumber) 684 | ) 685 | display_json("api.get()", api.get_activity_types()) 686 | for gear in gear: 687 | uuid = gear["uuid"] 688 | name = gear["displayName"] 689 | display_json( 690 | f"api.get_gear_stats({uuid}) / {name}", api.get_gear_stats(uuid) 691 | ) 692 | 693 | # WEIGHT-INS 694 | elif i == "B": 695 | # Get weigh-ins data 696 | display_json( 697 | f"api.get_weigh_ins({startdate.isoformat()}, {today.isoformat()})", 698 | api.get_weigh_ins(startdate.isoformat(), today.isoformat()), 699 | ) 700 | elif i == "C": 701 | # Get daily weigh-ins data 702 | display_json( 703 | f"api.get_daily_weigh_ins({today.isoformat()})", 704 | api.get_daily_weigh_ins(today.isoformat()), 705 | ) 706 | elif i == "D": 707 | # Delete weigh-ins data for today 708 | display_json( 709 | f"api.delete_weigh_ins({today.isoformat()}, delete_all=True)", 710 | api.delete_weigh_ins(today.isoformat(), delete_all=True), 711 | ) 712 | elif i == "E": 713 | # Add a weigh-in 714 | display_json( 715 | f"api.add_weigh_in(weight={weight}, unitKey={weightunit})", 716 | api.add_weigh_in(weight=weight, unitKey=weightunit), 717 | ) 718 | 719 | # Add a weigh-in with timestamps 720 | yesterday = today - datetime.timedelta(days=1) # Get yesterday's date 721 | weigh_in_date = datetime.datetime.strptime(yesterday.isoformat(), "%Y-%m-%d") 722 | local_timestamp = weigh_in_date.strftime('%Y-%m-%dT%H:%M:%S') 723 | gmt_timestamp = weigh_in_date.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S') 724 | 725 | display_json( 726 | f"api.add_weigh_in_with_timestamps(weight={weight}, unitKey={weightunit}, dateTimestamp={local_timestamp}, gmtTimestamp={gmt_timestamp})", 727 | api.add_weigh_in_with_timestamps( 728 | weight=weight, 729 | unitKey=weightunit, 730 | dateTimestamp=local_timestamp, 731 | gmtTimestamp=gmt_timestamp 732 | ) 733 | ) 734 | 735 | # CHALLENGES/EXPEDITIONS 736 | elif i == "F": 737 | # Get virtual challenges/expeditions 738 | display_json( 739 | f"api.get_inprogress_virtual_challenges({startdate.isoformat()}, {today.isoformat()})", 740 | api.get_inprogress_virtual_challenges( 741 | startdate.isoformat(), today.isoformat() 742 | ), 743 | ) 744 | elif i == "G": 745 | # Get hill score data 746 | display_json( 747 | f"api.get_hill_score({startdate.isoformat()}, {today.isoformat()})", 748 | api.get_hill_score(startdate.isoformat(), today.isoformat()), 749 | ) 750 | elif i == "H": 751 | # Get endurance score data 752 | display_json( 753 | f"api.get_endurance_score({startdate.isoformat()}, {today.isoformat()})", 754 | api.get_endurance_score(startdate.isoformat(), today.isoformat()), 755 | ) 756 | elif i == "I": 757 | # Get activities for date 758 | display_json( 759 | f"api.get_activities_fordate({today.isoformat()})", 760 | api.get_activities_fordate(today.isoformat()), 761 | ) 762 | elif i == "J": 763 | # Get race predictions 764 | display_json("api.get_race_predictions()", api.get_race_predictions()) 765 | elif i == "K": 766 | # Get all day stress data for date 767 | display_json( 768 | f"api.get_all_day_stress({today.isoformat()})", 769 | api.get_all_day_stress(today.isoformat()), 770 | ) 771 | elif i == "L": 772 | # Add body composition 773 | weight = 70.0 774 | percent_fat = 15.4 775 | percent_hydration = 54.8 776 | visceral_fat_mass = 10.8 777 | bone_mass = 2.9 778 | muscle_mass = 55.2 779 | basal_met = 1454.1 780 | active_met = None 781 | physique_rating = None 782 | metabolic_age = 33.0 783 | visceral_fat_rating = None 784 | bmi = 22.2 785 | display_json( 786 | f"api.add_body_composition({today.isoformat()}, {weight}, {percent_fat}, {percent_hydration}, {visceral_fat_mass}, {bone_mass}, {muscle_mass}, {basal_met}, {active_met}, {physique_rating}, {metabolic_age}, {visceral_fat_rating}, {bmi})", 787 | api.add_body_composition( 788 | today.isoformat(), 789 | weight=weight, 790 | percent_fat=percent_fat, 791 | percent_hydration=percent_hydration, 792 | visceral_fat_mass=visceral_fat_mass, 793 | bone_mass=bone_mass, 794 | muscle_mass=muscle_mass, 795 | basal_met=basal_met, 796 | active_met=active_met, 797 | physique_rating=physique_rating, 798 | metabolic_age=metabolic_age, 799 | visceral_fat_rating=visceral_fat_rating, 800 | bmi=bmi, 801 | ), 802 | ) 803 | elif i == "M": 804 | # Set blood pressure values 805 | display_json( 806 | "api.set_blood_pressure(120, 80, 80, notes=`Testing with example.py`)", 807 | api.set_blood_pressure( 808 | 120, 80, 80, notes="Testing with example.py" 809 | ), 810 | ) 811 | elif i == "N": 812 | # Get user profile 813 | display_json("api.get_user_profile()", api.get_user_profile()) 814 | elif i == "O": 815 | # Reload epoch data for date 816 | display_json( 817 | f"api.request_reload({today.isoformat()})", 818 | api.request_reload(today.isoformat()), 819 | ) 820 | 821 | # WORKOUTS 822 | elif i == "P": 823 | workouts = api.get_workouts() 824 | # Get workout 0-100 825 | display_json("api.get_workouts()", api.get_workouts()) 826 | 827 | # Get last fetched workout 828 | workout_id = workouts[-1]["workoutId"] 829 | workout_name = workouts[-1]["workoutName"] 830 | display_json( 831 | f"api.get_workout_by_id({workout_id})", 832 | api.get_workout_by_id(workout_id), 833 | ) 834 | 835 | # Download last fetched workout 836 | print(f"api.download_workout({workout_id})") 837 | workout_data = api.download_workout(workout_id) 838 | 839 | output_file = f"./{str(workout_name)}.fit" 840 | with open(output_file, "wb") as fb: 841 | fb.write(workout_data) 842 | print(f"Workout data downloaded to file {output_file}") 843 | 844 | # elif i == "Q": 845 | # display_json( 846 | # f"api.upload_workout({workout_example})", 847 | # api.upload_workout(workout_example)) 848 | 849 | # DAILY EVENTS 850 | elif i == "V": 851 | # Get all day wellness events for 7 days ago 852 | display_json( 853 | f"api.get_all_day_events({today.isoformat()})", 854 | api.get_all_day_events(startdate.isoformat()), 855 | ) 856 | # WOMEN'S HEALTH 857 | elif i == "S": 858 | # Get pregnancy summary data 859 | display_json("api.get_pregnancy_summary()", api.get_pregnancy_summary()) 860 | 861 | # Additional related calls: 862 | # get_menstrual_data_for_date(self, fordate: str): takes a single date and returns the Garmin Menstrual Summary data for that date 863 | # get_menstrual_calendar_data(self, startdate: str, enddate: str) takes two dates and returns summaries of cycles that have days between the two days 864 | 865 | elif i == "T": 866 | # Add hydration data for today 867 | value_in_ml = 240 868 | raw_date = datetime.date.today() 869 | cdate = str(raw_date) 870 | raw_ts = datetime.datetime.now() 871 | timestamp = datetime.datetime.strftime(raw_ts, "%Y-%m-%dT%H:%M:%S.%f") 872 | 873 | display_json( 874 | f"api.add_hydration_data(value_in_ml={value_in_ml},cdate='{cdate}',timestamp='{timestamp}')", 875 | api.add_hydration_data( 876 | value_in_ml=value_in_ml, cdate=cdate, timestamp=timestamp 877 | ), 878 | ) 879 | 880 | elif i == "U": 881 | # Get fitness age data 882 | display_json( 883 | f"api.get_fitnessage_data({today.isoformat()})", 884 | api.get_fitnessage_data(today.isoformat()), 885 | ) 886 | 887 | elif i == "W": 888 | # Get userprofile settings 889 | display_json( 890 | "api.get_userprofile_settings()", api.get_userprofile_settings() 891 | ) 892 | 893 | elif i == "Z": 894 | # Remove stored login tokens for Garmin Connect portal 895 | tokendir = os.path.expanduser(tokenstore) 896 | print(f"Removing stored login tokens from: {tokendir}") 897 | try: 898 | for root, dirs, files in os.walk(tokendir, topdown=False): 899 | for name in files: 900 | os.remove(os.path.join(root, name)) 901 | for name in dirs: 902 | os.rmdir(os.path.join(root, name)) 903 | print(f"Directory {tokendir} removed") 904 | except FileNotFoundError: 905 | print(f"Directory not found: {tokendir}") 906 | api = None 907 | 908 | except ( 909 | GarminConnectConnectionError, 910 | GarminConnectAuthenticationError, 911 | GarminConnectTooManyRequestsError, 912 | requests.exceptions.HTTPError, 913 | GarthHTTPError, 914 | ) as err: 915 | logger.error(err) 916 | except KeyError: 917 | # Invalid menu option chosen 918 | pass 919 | else: 920 | print("Could not login to Garmin Connect, try again later.") 921 | 922 | 923 | # Main program loop 924 | while True: 925 | # Display header and login 926 | print("\n*** Garmin Connect API Demo by cyberjunky ***\n") 927 | 928 | # Init API 929 | if not api: 930 | api = init_api(email, password) 931 | 932 | if api: 933 | # Display menu 934 | print_menu() 935 | option = readchar.readkey() 936 | switch(api, option) 937 | else: 938 | api = init_api(email, password) ```