Package: garth
GarthGarmin SSO auth + Connect Python clientGoogle ColabsStress: 28-day rolling averageStress levels from one day to another can vary by extremes, but there's always
a general trend. Using a scatter plot with a rolling average shows both the
individual days and the trend. The Colab retrieves up to three years of daily
data. If there's less than three years of data, it retrieves whatever is
available.Sleep analysis over 90 daysThe Garmin Connect app only shows a maximum of seven days for sleep
stages—making it hard to see trends. The Connect API supports retrieving
daily sleep quality in 28-day pages, but that doesn't show details. UsingSleedData.list()gives us the ability to retrieve an arbitrary number of
day with enough detail to product a stacked bar graph of the daily sleep
stages.One specific graph that's useful but not available in the Connect app is
sleep start and end times over an extended period. This provides context
to the sleep hours and stages.ChatGPT analysis of Garmin statsChatGPT's Advanced Data Analysis took can provide incredible insight
into the data in a way that's much simpler than using Pandas and Matplotlib.Start by using the linked Colab to download a CSV of the last three years
of your stats, and upload the CSV to ChatGPT.Here's the outputs of the following prompts:How do I sleep on different days of the week?On what days do I exercise the most?Magic!BackgroundGarth is meant for personal use and follows the philosophy that your data is
your data. You should be able to download it and analyze it in the way that
you'd like. In my case, that means processing with Google Colab, Pandas,
Matplotlib, etc.There are already a few Garmin Connect libraries. Why write another?Authentication and stabilityThe most important reasoning is to build a library with authentication that
works onGoogle Colaband doesn't require
tools like Cloudscraper. Garth, in comparison:Uses OAuth1 and OAuth2 token authentication after initial loginOAuth1 token survives for a yearSupports MFAAuto-refresh of OAuth2 token when expiredWorks on Google ColabUses Pydantic dataclasses to validate and simplify use of dataFull test coverageJSON vs HTMLUsinggarth.connectapi()allows you to make requests to the Connect API
and receive JSON vs needing to parse HTML. You can use the same endpoints the
mobile app uses.This also goes back to authentication. Garth manages the necessary Bearer
Authentication (along with auto-refresh) necessary to make requests routed to
the Connect API.InstructionsInstallpython-mpipinstallgarthClone, setup environment and run testsghrepoclonematin/garthcdgarth
makeinstall
makeUsemake helpto see all the options.Authenticate and save sessionimportgarthfromgetpassimportgetpassemail=input("Enter email address: ")password=getpass("Enter password: ")# If there's MFA, you'll be prompted during the logingarth.login(email,password)garth.save("~/.garth")ConfigureSet domain for Chinagarth.configure(domain="garmin.cn")Proxy through Charlesgarth.configure(proxies={"https":"http://localhost:8888"},ssl_verify=False)Attempt to resume sessionimportgarthfromgarthimportGarthExceptiongarth.resume("~/.garth")try:garth.client.usernameexceptGarthException:# Session is expired. You'll need to log in againConnect APIDaily detailssleep=garth.connectapi(f"/wellness-service/wellness/dailySleepData/{garth.client.username}",params={"date":"2023-07-05","nonSleepBufferMinutes":60},)list(sleep.keys())["dailySleepDTO","sleepMovement","remSleepData","sleepLevels","sleepRestlessMoments","restlessMomentsCount","wellnessSpO2SleepSummaryDTO","wellnessEpochSPO2DataDTOList","wellnessEpochRespirationDataDTOList","sleepStress"]Statsstress=garth.connectapi("/usersummary-service/stats/stress/weekly/2023-07-05/52"){"calendarDate":"2023-07-13","values":{"highStressDuration":2880,"lowStressDuration":10140,"overallStressLevel":33,"restStressDuration":30960,"mediumStressDuration":8760}}Uploadwithopen("12129115726_ACTIVITY.fit","rb")asf:uploaded=garth.client.upload(f)Note: Garmin doesn't accept uploads ofstructuredFIT files as outlined inthis conversation. FIT files
generated from workouts are accepted without issues.{'detailedImportResult':{'uploadId':212157427938,'uploadUuid':{'uuid':'6e56051d-1dd4-4f2c-b8ba-00a1a7d82eb3'},'owner':2591602,'fileSize':5289,'processingTime':36,'creationDate':'2023-09-29 01:58:19.113 GMT','ipAddress':None,'fileName':'12129115726_ACTIVITY.fit','report':None,'successes':[],'failures':[]}}Stats resourcesStressDaily stress levelsDailyStress.list("2023-07-23",2)[DailyStress(calendar_date=datetime.date(2023,7,22),overall_stress_level=31,rest_stress_duration=31980,low_stress_duration=23820,medium_stress_duration=7440,high_stress_duration=1500),DailyStress(calendar_date=datetime.date(2023,7,23),overall_stress_level=26,rest_stress_duration=38220,low_stress_duration=22500,medium_stress_duration=2520,high_stress_duration=300)]Weekly stress levelsWeeklyStress.list("2023-07-23",2)[WeeklyStress(calendar_date=datetime.date(2023,7,10),value=33),WeeklyStress(calendar_date=datetime.date(2023,7,17),value=32)]StepsDaily stepsgarth.DailySteps.list(period=2)[DailySteps(calendar_date=datetime.date(2023,7,28),total_steps=6510,total_distance=5552,step_goal=8090),DailySteps(calendar_date=datetime.date(2023,7,29),total_steps=7218,total_distance=6002,step_goal=7940)]Weekly stepsgarth.WeeklySteps.list(period=2)[WeeklySteps(calendar_date=datetime.date(2023,7,16),total_steps=42339,average_steps=6048.428571428572,average_distance=5039.285714285715,total_distance=35275.0,wellness_data_days_count=7),WeeklySteps(calendar_date=datetime.date(2023,7,23),total_steps=56420,average_steps=8060.0,average_distance=7198.142857142857,total_distance=50387.0,wellness_data_days_count=7)]Intensity MinutesDaily intensity minutesgarth.DailyIntensityMinutes.list(period=2)[DailyIntensityMinutes(calendar_date=datetime.date(2023,7,28),weekly_goal=150,moderate_value=0,vigorous_value=0),DailyIntensityMinutes(calendar_date=datetime.date(2023,7,29),weekly_goal=150,moderate_value=0,vigorous_value=0)]Weekly intensity minutesgarth.WeeklyIntensityMinutes.list(period=2)[WeeklyIntensityMinutes(calendar_date=datetime.date(2023,7,17),weekly_goal=150,moderate_value=103,vigorous_value=9),WeeklyIntensityMinutes(calendar_date=datetime.date(2023,7,24),weekly_goal=150,moderate_value=101,vigorous_value=105)]HRVDaily HRVgarth.DailyHRV.list(period=2)[DailyHRV(calendar_date=datetime.date(2023,7,28),weekly_avg=39,last_night_avg=36,last_night_5_min_high=52,baseline=HRVBaseline(low_upper=36,balanced_low=39,balanced_upper=51,marker_value=0.25),status='BALANCED',feedback_phrase='HRV_BALANCED_2',create_time_stamp=datetime.datetime(2023,7,28,12,40,16,785000)),DailyHRV(calendar_date=datetime.date(2023,7,29),weekly_avg=40,last_night_avg=41,last_night_5_min_high=76,baseline=HRVBaseline(low_upper=36,balanced_low=39,balanced_upper=51,marker_value=0.2916565),status='BALANCED',feedback_phrase='HRV_BALANCED_8',create_time_stamp=datetime.datetime(2023,7,29,13,45,23,479000))]Detailed HRV datagarth.HRVData.get("2023-07-20")HRVData(user_profile_pk=2591602,hrv_summary=HRVSummary(calendar_date=datetime.date(2023,7,20),weekly_avg=39,last_night_avg=42,last_night_5_min_high=66,baseline=Baseline(low_upper=36,balanced_low=39,balanced_upper=52,marker_value=0.25),status='BALANCED',feedback_phrase='HRV_BALANCED_7',create_time_stamp=datetime.datetime(2023,7,20,12,14,11,898000)),hrv_readings=[HRVReading(hrv_value=54,reading_time_gmt=datetime.datetime(2023,7,20,5,29,48),reading_time_local=datetime.datetime(2023,7,19,23,29,48)),HRVReading(hrv_value=56,reading_time_gmt=datetime.datetime(2023,7,20,5,34,48),reading_time_local=datetime.datetime(2023,7,19,23,34,48)),# ... truncated for brevityHRVReading(hrv_value=38,reading_time_gmt=datetime.datetime(2023,7,20,12,9,48),reading_time_local=datetime.datetime(2023,7,20,6,9,48))],start_timestamp_gmt=datetime.datetime(2023,7,20,5,25),end_timestamp_gmt=datetime.datetime(2023,7,20,12,9,48),start_timestamp_local=datetime.datetime(2023,7,19,23,25),end_timestamp_local=datetime.datetime(2023,7,20,6,9,48),sleep_start_timestamp_gmt=datetime.datetime(2023,7,20,5,25),sleep_end_timestamp_gmt=datetime.datetime(2023,7,20,12,11),sleep_start_timestamp_local=datetime.datetime(2023,7,19,23,25),sleep_end_timestamp_local=datetime.datetime(2023,7,20,6,11))SleepDaily sleep qualitygarth.DailySleep.list("2023-07-23",2)[DailySleep(calendar_date=datetime.date(2023,7,22),value=69),DailySleep(calendar_date=datetime.date(2023,7,23),value=73)]Detailed sleep datagarth.SleepData.get("2023-07-20")SleepData(daily_sleep_dto=DailySleepDTO(id=1689830700000,user_profile_pk=2591602,calendar_date=datetime.date(2023,7,20),sleep_time_seconds=23700,nap_time_seconds=0,sleep_window_confirmed=True,sleep_window_confirmation_type='enhanced_confirmed_final',sleep_start_timestamp_gmt=datetime.datetime(2023,7,20,5,25,tzinfo=TzInfo(UTC)),sleep_end_timestamp_gmt=datetime.datetime(2023,7,20,12,11,tzinfo=TzInfo(UTC)),sleep_start_timestamp_local=datetime.datetime(2023,7,19,23,25,tzinfo=TzInfo(UTC)),sleep_end_timestamp_local=datetime.datetime(2023,7,20,6,11,tzinfo=TzInfo(UTC)),unmeasurable_sleep_seconds=0,deep_sleep_seconds=9660,light_sleep_seconds=12600,rem_sleep_seconds=1440,awake_sleep_seconds=660,device_rem_capable=True,retro=False,sleep_from_device=True,sleep_version=2,awake_count=1,sleep_scores=SleepScores(total_duration=Score(qualifier_key='FAIR',optimal_start=28800.0,optimal_end=28800.0,value=None,ideal_start_in_seconds=None,deal_end_in_seconds=None),stress=Score(qualifier_key='FAIR',optimal_start=0.0,optimal_end=15.0,value=None,ideal_start_in_seconds=None,ideal_end_in_seconds=None),awake_count=Score(qualifier_key='GOOD',optimal_start=0.0,optimal_end=1.0,value=None,ideal_start_in_seconds=None,ideal_end_in_seconds=None),overall=Score(qualifier_key='FAIR',optimal_start=None,optimal_end=None,value=68,ideal_start_in_seconds=None,ideal_end_in_seconds=None),rem_percentage=Score(qualifier_key='POOR',optimal_start=21.0,optimal_end=31.0,value=6,ideal_start_in_seconds=4977.0,ideal_end_in_seconds=7347.0),restlessness=Score(qualifier_key='EXCELLENT',optimal_start=0.0,optimal_end=5.0,value=None,ideal_start_in_seconds=None,ideal_end_in_seconds=None),light_percentage=Score(qualifier_key='EXCELLENT',optimal_start=30.0,optimal_end=64.0,value=53,ideal_start_in_seconds=7110.0,ideal_end_in_seconds=15168.0),deep_percentage=Score(qualifier_key='EXCELLENT',optimal_start=16.0,optimal_end=33.0,value=41,ideal_start_in_seconds=3792.0,ideal_end_in_seconds=7821.0)),auto_sleep_start_timestamp_gmt=None,auto_sleep_end_timestamp_gmt=None,sleep_quality_type_pk=None,sleep_result_type_pk=None,average_sp_o2_value=92.0,lowest_sp_o2_value=87,highest_sp_o2_value=100,average_sp_o2_hr_sleep=53.0,average_respiration_value=14.0,lowest_respiration_value=12.0,highest_respiration_value=16.0,avg_sleep_stress=17.0,age_group='ADULT',sleep_score_feedback='NEGATIVE_NOT_ENOUGH_REM',sleep_score_insight='NONE'),sleep_movement=[SleepMovement(start_gmt=datetime.datetime(2023,7,20,4,25),end_gmt=datetime.datetime(2023,7,20,4,26),activity_level=5.688743692980419),SleepMovement(start_gmt=datetime.datetime(2023,7,20,4,26),end_gmt=datetime.datetime(2023,7,20,4,27),activity_level=5.318763075304898),# ... truncated for brevitySleepMovement(start_gmt=datetime.datetime(2023,7,20,13,10),end_gmt=datetime.datetime(2023,7,20,13,11),activity_level=7.088729101943337)])List sleep data over several nights.garth.SleepData.list("2023-07-20",30)UserUserProfilegarth.UserProfile.get()UserProfile(id=3154645,profile_id=2591602,garmin_guid="0690cc1d-d23d-4412-b027-80fd4ed1c0f6",display_name="mtamizi",full_name="Matin Tamizi",user_name="mtamizi",profile_image_uuid="73240e81-6e4d-43fc-8af8-c8f6c51b3b8f",profile_image_url_large=("https://s3.amazonaws.com/garmin-connect-prod/profile_images/""73240e81-6e4d-43fc-8af8-c8f6c51b3b8f-2591602.png"),profile_image_url_medium=("https://s3.amazonaws.com/garmin-connect-prod/profile_images/""685a19e9-a7be-4a11-9bf9-faca0c5d1f1a-2591602.png"),profile_image_url_small=("https://s3.amazonaws.com/garmin-connect-prod/profile_images/""6302f021-0ec7-4dc9-b0c3-d5a19bc5a08c-2591602.png"),location="Ciudad de México, CDMX",facebook_url=None,twitter_url=None,personal_website=None,motivation=None,bio=None,primary_activity=None,favorite_activity_types=[],running_training_speed=0.0,cycling_training_speed=0.0,favorite_cycling_activity_types=[],cycling_classification=None,cycling_max_avg_power=0.0,swimming_training_speed=0.0,profile_visibility="private",activity_start_visibility="private",activity_map_visibility="public",course_visibility="public",activity_heart_rate_visibility="public",activity_power_visibility="public",badge_visibility="private",show_age=False,show_weight=False,show_height=False,show_weight_class=False,show_age_range=False,show_gender=False,show_activity_class=False,show_vo_2_max=False,show_personal_records=False,show_last_12_months=False,show_lifetime_totals=False,show_upcoming_events=False,show_recent_favorites=False,show_recent_device=False,show_recent_gear=False,show_badges=True,other_activity=None,other_primary_activity=None,other_motivation=None,user_roles=["SCOPE_ATP_READ","SCOPE_ATP_WRITE","SCOPE_COMMUNITY_COURSE_READ","SCOPE_COMMUNITY_COURSE_WRITE","SCOPE_CONNECT_READ","SCOPE_CONNECT_WRITE","SCOPE_DT_CLIENT_ANALYTICS_WRITE","SCOPE_GARMINPAY_READ","SCOPE_GARMINPAY_WRITE","SCOPE_GCOFFER_READ","SCOPE_GCOFFER_WRITE","SCOPE_GHS_SAMD","SCOPE_GHS_UPLOAD","SCOPE_GOLF_API_READ","SCOPE_GOLF_API_WRITE","SCOPE_INSIGHTS_READ","SCOPE_INSIGHTS_WRITE","SCOPE_PRODUCT_SEARCH_READ","ROLE_CONNECTUSER","ROLE_FITNESS_USER","ROLE_WELLNESS_USER","ROLE_OUTDOOR_USER","ROLE_CONNECT_2_USER","ROLE_TACX_APP_USER",],name_approved=True,user_profile_full_name="Matin Tamizi",make_golf_scorecards_private=True,allow_golf_live_scoring=False,allow_golf_scoring_by_connections=True,user_level=3,user_point=118,level_update_date="2020-12-12T15:20:38.0",level_is_viewed=False,level_point_threshold=140,user_point_offset=0,user_pro=False,)UserSettingsgarth.UserSettings.get()UserSettings(id=2591602,user_data=UserData(gender="MALE",weight=83000.0,height=182.0,time_format="time_twenty_four_hr",birth_date=datetime.date(1984,10,17),measurement_system="metric",activity_level=None,handedness="RIGHT",power_format=PowerFormat(format_id=30,format_key="watt",min_fraction=0,max_fraction=0,grouping_used=True,display_format=None,),heart_rate_format=PowerFormat(format_id=21,format_key="bpm",min_fraction=0,max_fraction=0,grouping_used=False,display_format=None,),first_day_of_week=FirstDayOfWeek(day_id=2,day_name="sunday",sort_order=2,is_possible_first_day=True,),vo_2_max_running=45.0,vo_2_max_cycling=None,lactate_threshold_speed=0.34722125000000004,lactate_threshold_heart_rate=None,dive_number=None,intensity_minutes_calc_method="AUTO",moderate_intensity_minutes_hr_zone=3,vigorous_intensity_minutes_hr_zone=4,hydration_measurement_unit="milliliter",hydration_containers=[],hydration_auto_goal_enabled=True,firstbeat_max_stress_score=None,firstbeat_cycling_lt_timestamp=None,firstbeat_running_lt_timestamp=1044719868,threshold_heart_rate_auto_detected=True,ftp_auto_detected=None,training_status_paused_date=None,weather_location=None,golf_distance_unit="statute_us",golf_elevation_unit=None,golf_speed_unit=None,external_bottom_time=None,),user_sleep=UserSleep(sleep_time=80400,default_sleep_time=False,wake_time=24000,default_wake_time=False,),connect_date=None,source_type=None,)