@@ -54,6 +54,13 @@ def __init__(
5454 self .garmin_connect_metrics_url = (
5555 "/metrics-service/metrics/maxmet/daily"
5656 )
57+ self .garmin_connect_biometric_url = (
58+ "/biometric-service/biometric"
59+ )
60+
61+ self .garmin_connect_biometric_stats_url = (
62+ "/biometric-service/stats"
63+ )
5764 self .garmin_connect_daily_hydration_url = (
5865 "/usersummary-service/usersummary/hydration/daily"
5966 )
@@ -618,6 +625,88 @@ def get_max_metrics(self, cdate: str) -> Dict[str, Any]:
618625
619626 return self .connectapi (url )
620627
628+ def get_lactate_threshold (self , * ,latest : bool = True , start_date : Optional [str | date ]= None , end_date : Optional [str | date ]= None , aggregation : str = "daily" ) -> Dict :
629+ """
630+ Returns Running Lactate Threshold information, including heart rate, power, and speed
631+
632+ :param bool required - latest: Whether to query for the latest Lactate Threshold info or a range. False if querying a range
633+ :param date optional - start_date: The first date in the range to query, format 'YYYY-MM-DD'. Required if `latest` is False. Ignored if `latest` is True
634+ :param date optional - end_date: The last date in the range to query, format 'YYYY-MM-DD'. Defaults to current data. Ignored if `latest` is True
635+ :param str optional - aggregation: How to aggregate the data. Must be one of `daily`, `weekly`, `monthly`, `yearly`.
636+
637+ """
638+
639+ if latest :
640+
641+ speed_and_heart_rate_url = f"{ self .garmin_connect_biometric_url } /latestLactateThreshold"
642+ power_url = f"{ self .garmin_connect_biometric_url } /powerToWeight/latest/{ date .today ()} ?sport=Running"
643+
644+ power = self .connectapi (power_url )
645+ try :
646+ power_dict = power [0 ]
647+ except IndexError :
648+ # If no power available
649+ power_dict = {}
650+
651+ speed_and_heart_rate = self .connectapi (speed_and_heart_rate_url )
652+
653+ speed_and_heart_rate_dict = {
654+ "userProfilePK" : None ,
655+ "version" : None ,
656+ "calendarDate" : None ,
657+ "sequence" : None ,
658+ "speed" : None ,
659+ "heartRate" : None ,
660+ "heartRateCycling" : None
661+ }
662+
663+ # Garmin /latestLactateThreshold endpoint returns a list of two
664+ # (or more, if cyclingHeartRate ever gets values) nearly identical dicts.
665+ # We're combining them here
666+ for entry in speed_and_heart_rate :
667+ if entry ['speed' ] is not None :
668+ speed_and_heart_rate_dict ["userProfilePK" ] = entry ["userProfilePK" ]
669+ speed_and_heart_rate_dict ["version" ] = entry ["version" ]
670+ speed_and_heart_rate_dict ["calendarDate" ] = entry ["calendarDate" ]
671+ speed_and_heart_rate_dict ["sequence" ] = entry ["sequence" ]
672+ speed_and_heart_rate_dict ["speed" ] = entry ["speed" ]
673+
674+ # This is not a typo. The Garmin dictionary has a typo as of 2025-07-08, refering to it as "hearRate"
675+ elif entry ['hearRate' ] is not None :
676+ speed_and_heart_rate_dict ["heartRate" ] = entry ["hearRate" ] # Fix Garmin's typo
677+
678+ # Doesn't exist for me but adding it just in case. We'll check for each entry
679+ if entry ['heartRateCycling' ] is not None :
680+ speed_and_heart_rate_dict ["heartRateCycling" ] = entry ["heartRateCycling" ]
681+
682+ return {
683+ "speed_and_heart_rate" : speed_and_heart_rate_dict ,
684+ "power" : power_dict
685+ }
686+
687+
688+ if start_date is None :
689+ raise ValueError ("You must either specify 'latest=True' or a start_date" )
690+
691+ if end_date is None :
692+ end_date = date .today ().isoformat ()
693+
694+ _valid_aggregations = {"daily" , "weekly" , "monthly" , "yearly" }
695+ if aggregation not in _valid_aggregations :
696+ raise ValueError (f"aggregation must be one of { _valid_aggregations } " )
697+
698+ speed_url = f"{ self .garmin_connect_biometric_stats_url } /lactateThresholdSpeed/range/{ start_date } /{ end_date } ?sport=RUNNING&aggregation={ aggregation } &aggregationStrategy=LATEST"
699+
700+ heart_rate_url = f"{ self .garmin_connect_biometric_stats_url } /lactateThresholdHeartRate/range/{ start_date } /{ end_date } ?sport=RUNNING&aggregation={ aggregation } &aggregationStrategy=LATEST"
701+
702+ power_url = f"{ self .garmin_connect_biometric_stats_url } /functionalThresholdPower/range/{ start_date } /{ end_date } ?sport=RUNNING&aggregation={ aggregation } &aggregationStrategy=LATEST"
703+
704+ speed = self .connectapi (speed_url )
705+ heart_rate = self .connectapi (heart_rate_url )
706+ power = self .connectapi (power_url )
707+
708+ return {"speed" : speed , "heart_rate" : heart_rate , "power" : power }
709+
621710 def add_hydration_data (
622711 self , value_in_ml : float , timestamp = None , cdate : Optional [str ] = None
623712 ) -> Dict [str , Any ]:
0 commit comments