Skip to content

Commit 5e4fe6b

Browse files
authored
Merge pull request #285 from nickknissen/feat-traning-plans
Add training plans API support
2 parents 8d4f2e6 + 2cf259f commit 5e4fe6b

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
The Garmin Connect API library comes with two examples:
44

55
- **`example.py`** - Simple getting-started example showing authentication, token storage, and basic API calls
6-
- **`demo.py`** - Comprehensive demo providing access to **100+ API methods** organized into **11 categories** for easy navigation
6+
- **`demo.py`** - Comprehensive demo providing access to **100+ API methods** organized into **12 categories** for easy navigation
77

88
Note: The demo menu is generated dynamically; exact options may change between releases.
99

@@ -24,6 +24,7 @@ Select a category:
2424
[9] 🎽 Gear & Equipment
2525
[0] 💧 Hydration & Wellness
2626
[a] 🔧 System & Export
27+
[b] 📅 Training plans
2728

2829
[q] Exit program
2930

@@ -45,6 +46,7 @@ Make your selection:
4546
- **Gear & Equipment**: 8 methods (gear management, tracking)
4647
- **Hydration & Wellness**: 9 methods (hydration, blood pressure, menstrual)
4748
- **System & Export**: 4 methods (reporting, logout, GraphQL)
49+
- **Training Plans**: 3 methods
4850

4951
### Interactive Features
5052

demo.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,13 @@ def __init__(self):
417417
"4": {"desc": "Execute GraphQL query", "key": "query_garmin_graphql"},
418418
},
419419
},
420+
"b": {
421+
"name": "📅 Training Plans",
422+
"options": {
423+
"1": {"desc": "Get training plans", "key": "get_training_plans"},
424+
"2": {"desc": "Get training plan by ID", "key": "get_training_plan_by_id"},
425+
},
426+
},
420427
}
421428

422429
current_category = None
@@ -1769,6 +1776,63 @@ def get_activity_exercise_sets_data(api: Garmin) -> None:
17691776
print("ℹ️ No activity exercise sets available")
17701777

17711778

1779+
def get_training_plan_by_id_data(api: Garmin) -> None:
1780+
"""Get training plan details by ID (routes FBT_ADAPTIVE plans to the adaptive endpoint)."""
1781+
resp = api.get_training_plans() or {}
1782+
training_plans = resp.get("trainingPlanList") or []
1783+
if not training_plans:
1784+
print("ℹ️ No training plans found")
1785+
return
1786+
1787+
user_input = input("Enter training plan ID (press Enter for most recent): ").strip()
1788+
selected = None
1789+
if user_input:
1790+
try:
1791+
wanted_id = int(user_input)
1792+
selected = next(
1793+
(
1794+
p
1795+
for p in training_plans
1796+
if int(p.get("trainingPlanId", 0)) == wanted_id
1797+
),
1798+
None,
1799+
)
1800+
if not selected:
1801+
print(
1802+
f"ℹ️ Plan ID {wanted_id} not found in your plans; attempting fetch anyway"
1803+
)
1804+
plan_id = wanted_id
1805+
plan_name = f"Plan {wanted_id}"
1806+
plan_category = None
1807+
else:
1808+
plan_id = int(selected["trainingPlanId"])
1809+
plan_name = selected.get("name", str(plan_id))
1810+
plan_category = selected.get("trainingPlanCategory")
1811+
except ValueError:
1812+
print("❌ Invalid plan ID")
1813+
return
1814+
else:
1815+
selected = training_plans[-1]
1816+
plan_id = int(selected["trainingPlanId"])
1817+
plan_name = selected.get("name", str(plan_id))
1818+
plan_category = selected.get("trainingPlanCategory")
1819+
1820+
if plan_category == "FBT_ADAPTIVE":
1821+
call_and_display(
1822+
api.get_adaptive_training_plan_by_id,
1823+
plan_id,
1824+
method_name="get_adaptive_training_plan_by_id",
1825+
api_call_desc=f"api.get_adaptive_training_plan_by_id({plan_id}) - {plan_name}",
1826+
)
1827+
else:
1828+
call_and_display(
1829+
api.get_training_plan_by_id,
1830+
plan_id,
1831+
method_name="get_training_plan_by_id",
1832+
api_call_desc=f"api.get_training_plan_by_id({plan_id}) - {plan_name}",
1833+
)
1834+
1835+
17721836
def get_workout_by_id_data(api: Garmin) -> None:
17731837
"""Get workout by ID for the last workout."""
17741838
try:
@@ -3186,6 +3250,12 @@ def execute_api_call(api: Garmin, key: str) -> None:
31863250
method_name="get_workouts",
31873251
api_call_desc="api.get_workouts()",
31883252
),
3253+
"get_training_plan_by_id": lambda: get_training_plan_by_id_data(api),
3254+
"get_training_plans": lambda: call_and_display(
3255+
api.get_training_plans,
3256+
method_name="get_training_plans",
3257+
api_call_desc="api.get_training_plans()",
3258+
),
31893259
"upload_activity": lambda: upload_activity_file(api),
31903260
"download_activities": lambda: download_activities_by_date(api),
31913261
"get_activity_splits": lambda: get_activity_splits_data(api),

garminconnect/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ def __init__(
266266

267267
self.garmin_graphql_endpoint = "graphql-gateway/graphql"
268268

269+
self.garmin_connect_training_plan_url = "/trainingplan-service/trainingplan"
270+
269271
self.garth = garth.Client(
270272
domain="garmin.cn" if is_cn else "garmin.com",
271273
pool_connections=20,
@@ -2209,6 +2211,31 @@ def logout(self) -> None:
22092211
"Deprecated: Alternative is to delete the login tokens to logout."
22102212
)
22112213

2214+
def get_training_plans(self) -> dict[str, Any]:
2215+
"""Return all available training plans."""
2216+
2217+
url = f"{self.garmin_connect_training_plan_url}/plans"
2218+
logger.debug("Requesting training plans.")
2219+
return self.connectapi(url)
2220+
2221+
def get_training_plan_by_id(self, plan_id: int | str) -> dict[str, Any]:
2222+
"""Return details for a specific training plan."""
2223+
2224+
plan_id = _validate_positive_integer(int(plan_id), "plan_id")
2225+
2226+
url = f"{self.garmin_connect_training_plan_url}/plans/{plan_id}"
2227+
logger.debug("Requesting training plan details for %s", plan_id)
2228+
return self.connectapi(url)
2229+
2230+
def get_adaptive_training_plan_by_id(self, plan_id: int | str) -> dict[str, Any]:
2231+
"""Return details for a specific adaptive training plan."""
2232+
2233+
plan_id = _validate_positive_integer(int(plan_id), "plan_id")
2234+
url = f"{self.garmin_connect_training_plan_url}/fbt-adaptive/{plan_id}"
2235+
2236+
logger.debug("Requesting adaptive training plan details for %s", plan_id)
2237+
return self.connectapi(url)
2238+
22122239

22132240
class GarminConnectConnectionError(Exception):
22142241
"""Raised when communication ended in error."""

0 commit comments

Comments
 (0)