Skip to content

Commit 0caa04a

Browse files
authored
Merge pull request #271 from psdupvi/master
Add Lactate Threshold endpoints
2 parents 0685700 + e0cfec1 commit 0caa04a

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

example.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
# 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,...
4343
today = datetime.date.today()
4444
startdate = today - datetime.timedelta(days=7) # Select past week
45+
startdate_four_weeks = today - datetime.timedelta(days=28)
4546
start = 0
4647
limit = 100
4748
start_badge = 1 # Badge related calls calls start counting at 1
@@ -141,6 +142,7 @@
141142
"U": f"Get Fitness Age data for {today.isoformat()}",
142143
"V": f"Get daily wellness events data for {startdate.isoformat()}",
143144
"W": "Get userprofile settings",
145+
"X": "Get lactate threshold data, both Latest and for the past four weeks",
144146
"Z": "Remove stored login tokens (logout)",
145147
"q": "Exit",
146148
}
@@ -908,6 +910,14 @@ def switch(api, i):
908910
"api.get_userprofile_settings()", api.get_userprofile_settings()
909911
)
910912

913+
elif i == "X":
914+
# Get latest lactate threshold
915+
display_json(
916+
"api.get_lactate_threshold(latest=True)", api.get_lactate_threshold(latest=True)
917+
)
918+
# Get historical lactate threshold for past four weeks
919+
display_json(f"api.get_lactate_threshold(latest=False, start_date='{startdate_four_weeks.isoformat()}', end_date='{today.isoformat()}', aggregation='daily')", api.get_lactate_threshold(latest=False, start_date=startdate_four_weeks.isoformat(),
920+
end_date=today.isoformat(), aggregation="daily"), )
911921
elif i == "Z":
912922
# Remove stored login tokens for Garmin Connect portal
913923
tokendir = os.path.expanduser(tokenstore)

garminconnect/__init__.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)