#!/usr/bin/env python3 """ šŸƒā€ā™‚ļø Simple Garmin Connect API Example ===================================== This example demonstrates the basic usage of python-garminconnect: - Authentication with email/password - Token storage and automatic reuse - MFA (Multi-Factor Authentication) support - Comprehensive error handling for all API calls - Basic API calls for user stats For a comprehensive demo of all available API calls, see demo.py Dependencies: pip3 install garth requests Environment Variables (optional): export EMAIL= export PASSWORD= export GARMINTOKENS= """ import logging import os import sys from datetime import date from getpass import getpass from pathlib import Path import requests from garth.exc import GarthException, GarthHTTPError from garminconnect import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, ) # Suppress garminconnect library logging to avoid tracebacks in normal operation logging.getLogger("garminconnect").setLevel(logging.CRITICAL) def safe_api_call(api_method, *args, **kwargs): """ Safe API call wrapper with comprehensive error handling. This demonstrates the error handling patterns used throughout the library. Returns (success: bool, result: Any, error_message: str) """ try: result = api_method(*args, **kwargs) return True, result, None except GarthHTTPError as e: # Handle specific HTTP errors gracefully error_str = str(e) status_code = getattr(getattr(e, "response", None), "status_code", None) if status_code == 400 or "400" in error_str: return ( False, None, "Endpoint not available (400 Bad Request) - Feature may not be enabled for your account", ) elif status_code == 401 or "401" in error_str: return ( False, None, "Authentication required (401 Unauthorized) - Please re-authenticate", ) elif status_code == 403 or "403" in error_str: return ( False, None, "Access denied (403 Forbidden) - Account may not have permission", ) elif status_code == 404 or "404" in error_str: return ( False, None, "Endpoint not found (404) - Feature may have been moved or removed", ) elif status_code == 429 or "429" in error_str: return ( False, None, "Rate limit exceeded (429) - Please wait before making more requests", ) elif status_code == 500 or "500" in error_str: return ( False, None, "Server error (500) - Garmin's servers are experiencing issues", ) elif status_code == 503 or "503" in error_str: return ( False, None, "Service unavailable (503) - Garmin's servers are temporarily unavailable", ) else: return False, None, f"HTTP error: {e}" except FileNotFoundError: return ( False, None, "No valid tokens found. Please login with your email/password to create new tokens.", ) except GarminConnectAuthenticationError as e: return False, None, f"Authentication issue: {e}" except GarminConnectConnectionError as e: return False, None, f"Connection issue: {e}" except GarminConnectTooManyRequestsError as e: return False, None, f"Rate limit exceeded: {e}" except Exception as e: return False, None, f"Unexpected error: {e}" def get_credentials(): """Get email and password from environment or user input.""" email = os.getenv("EMAIL") password = os.getenv("PASSWORD") if not email: email = input("Login email: ") if not password: password = getpass("Enter password: ") return email, password def init_api() -> Garmin | None: """Initialize Garmin API with authentication and token management.""" # Configure token storage tokenstore = os.getenv("GARMINTOKENS", "~/.garminconnect") tokenstore_path = Path(tokenstore).expanduser() print(f"šŸ” Token storage: {tokenstore_path}") # Check if token files exist if tokenstore_path.exists(): print("šŸ“„ Found existing token directory") token_files = list(tokenstore_path.glob("*.json")) if token_files: print( f"šŸ”‘ Found {len(token_files)} token file(s): {[f.name for f in token_files]}" ) else: print("āš ļø Token directory exists but no token files found") else: print("šŸ“­ No existing token directory found") # First try to login with stored tokens try: print("šŸ”„ Attempting to use saved authentication tokens...") garmin = Garmin() garmin.login(str(tokenstore_path)) print("āœ… Successfully logged in using saved tokens!") return garmin except ( FileNotFoundError, GarthHTTPError, GarminConnectAuthenticationError, GarminConnectConnectionError, ): print("šŸ”‘ No valid tokens found. Requesting fresh login credentials.") # Loop for credential entry with retry on auth failure while True: try: # Get credentials email, password = get_credentials() print("ļæ½ Logging in with credentials...") garmin = Garmin( email=email, password=password, is_cn=False, return_on_mfa=True ) result1, result2 = garmin.login() if result1 == "needs_mfa": print("šŸ” Multi-factor authentication required") mfa_code = input("Please enter your MFA code: ") print("šŸ”„ Submitting MFA code...") try: garmin.resume_login(result2, mfa_code) print("āœ… MFA authentication successful!") except GarthHTTPError as garth_error: # Handle specific HTTP errors from MFA error_str = str(garth_error) if "429" in error_str and "Too Many Requests" in error_str: print("āŒ Too many MFA attempts") print("šŸ’” Please wait 30 minutes before trying again") sys.exit(1) elif "401" in error_str or "403" in error_str: print("āŒ Invalid MFA code") print("šŸ’” Please verify your MFA code and try again") continue else: # Other HTTP errors - don't retry print(f"āŒ MFA authentication failed: {garth_error}") sys.exit(1) except GarthException as garth_error: print(f"āŒ MFA authentication failed: {garth_error}") print("šŸ’” Please verify your MFA code and try again") continue # Save tokens for future use garmin.garth.dump(str(tokenstore_path)) print(f"šŸ’¾ Authentication tokens saved to: {tokenstore_path}") print("āœ… Login successful!") return garmin except GarminConnectAuthenticationError: print("āŒ Authentication failed:") print("šŸ’” Please check your username and password and try again") # Continue the loop to retry continue except ( FileNotFoundError, GarthHTTPError, GarminConnectConnectionError, requests.exceptions.HTTPError, ) as err: print(f"āŒ Connection error: {err}") print("šŸ’” Please check your internet connection and try again") return None except KeyboardInterrupt: print("\nšŸ‘‹ Cancelled by user") return None def display_user_info(api: Garmin): """Display basic user information with proper error handling.""" print("\n" + "=" * 60) print("šŸ‘¤ User Information") print("=" * 60) # Get user's full name success, full_name, error_msg = safe_api_call(api.get_full_name) if success: print(f"šŸ“ Name: {full_name}") else: print(f"šŸ“ Name: āš ļø {error_msg}") # Get user profile number from device info success, device_info, error_msg = safe_api_call(api.get_device_last_used) if success and device_info and device_info.get("userProfileNumber"): user_profile_number = device_info.get("userProfileNumber") print(f"šŸ†” Profile Number: {user_profile_number}") else: if not success: print(f"šŸ†” Profile Number: āš ļø {error_msg}") else: print("šŸ†” Profile Number: Not available") def display_daily_stats(api: Garmin): """Display today's activity statistics with proper error handling.""" today = date.today().isoformat() print("\n" + "=" * 60) print(f"šŸ“Š Daily Stats for {today}") print("=" * 60) # Get user summary (steps, calories, etc.) success, summary, error_msg = safe_api_call(api.get_user_summary, today) if success and summary: steps = summary.get("totalSteps", 0) distance = summary.get("totalDistanceMeters", 0) / 1000 # Convert to km calories = summary.get("totalKilocalories", 0) floors = summary.get("floorsClimbed", 0) print(f"šŸ‘£ Steps: {steps:,}") print(f"šŸ“ Distance: {distance:.2f} km") print(f"šŸ”„ Calories: {calories}") print(f"šŸ¢ Floors: {floors}") # Fun motivation based on steps if steps < 5000: print("🐌 Time to get those legs moving!") elif steps > 15000: print("šŸƒā€ā™‚ļø You're crushing it today!") else: print("šŸ‘ Nice progress! Keep it up!") else: if not success: print(f"āš ļø Could not fetch daily stats: {error_msg}") else: print("āš ļø No activity summary available for today") # Get hydration data success, hydration, error_msg = safe_api_call(api.get_hydration_data, today) if success and hydration and hydration.get("valueInML"): hydration_ml = int(hydration.get("valueInML", 0)) hydration_goal = hydration.get("goalInML", 0) hydration_cups = round(hydration_ml / 240, 1) # 240ml = 1 cup print(f"šŸ’§ Hydration: {hydration_ml}ml ({hydration_cups} cups)") if hydration_goal > 0: hydration_percent = round((hydration_ml / hydration_goal) * 100) print(f"šŸŽÆ Goal Progress: {hydration_percent}% of {hydration_goal}ml") else: if not success: print(f"šŸ’§ Hydration: āš ļø {error_msg}") else: print("šŸ’§ Hydration: No data available") def main(): """Main example demonstrating basic Garmin Connect API usage.""" print("šŸƒā€ā™‚ļø Simple Garmin Connect API Example") print("=" * 60) # Initialize API with authentication (will only prompt for credentials if needed) api = init_api() if not api: print("āŒ Failed to initialize API. Exiting.") return # Display user information display_user_info(api) # Display daily statistics display_daily_stats(api) print("\n" + "=" * 60) print("āœ… Example completed successfully!") print("šŸ’” For a comprehensive demo of all API features, run: python demo.py") print("=" * 60) if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\n🚪 Exiting example. Goodbye! šŸ‘‹") except Exception as e: print(f"\nāŒ Unexpected error: {e}")