garminconnect 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
"""Python 3 API wrapper for Garmin Connect to get your statistics."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import garth
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Garmin:
|
|
15
|
+
"""Class for fetching data from Garmin Connect."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, email=None, password=None, is_cn=False):
|
|
18
|
+
"""Create a new class instance."""
|
|
19
|
+
self.username = email
|
|
20
|
+
self.password = password
|
|
21
|
+
self.is_cn = is_cn
|
|
22
|
+
|
|
23
|
+
self.garmin_connect_devices_url = (
|
|
24
|
+
"/device-service/deviceregistration/devices"
|
|
25
|
+
)
|
|
26
|
+
self.garmin_connect_device_url = "/device-service/deviceservice"
|
|
27
|
+
self.garmin_connect_weight_url = "/weight-service"
|
|
28
|
+
self.garmin_connect_daily_summary_url = (
|
|
29
|
+
"/usersummary-service/usersummary/daily"
|
|
30
|
+
)
|
|
31
|
+
self.garmin_connect_metrics_url = (
|
|
32
|
+
"/metrics-service/metrics/maxmet/daily"
|
|
33
|
+
)
|
|
34
|
+
self.garmin_connect_daily_hydration_url = (
|
|
35
|
+
"/usersummary-service/usersummary/hydration/daily"
|
|
36
|
+
)
|
|
37
|
+
self.garmin_connect_daily_stats_steps_url = (
|
|
38
|
+
"/usersummary-service/stats/steps/daily"
|
|
39
|
+
)
|
|
40
|
+
self.garmin_connect_personal_record_url = (
|
|
41
|
+
"/personalrecord-service/personalrecord/prs"
|
|
42
|
+
)
|
|
43
|
+
self.garmin_connect_earned_badges_url = "/badge-service/badge/earned"
|
|
44
|
+
self.garmin_connect_adhoc_challenges_url = (
|
|
45
|
+
"/adhocchallenge-service/adHocChallenge/historical"
|
|
46
|
+
)
|
|
47
|
+
self.garmin_connect_badge_challenges_url = (
|
|
48
|
+
"/badgechallenge-service/badgeChallenge/completed"
|
|
49
|
+
)
|
|
50
|
+
self.garmin_connect_available_badge_challenges_url = (
|
|
51
|
+
"/badgechallenge-service/badgeChallenge/available"
|
|
52
|
+
)
|
|
53
|
+
self.garmin_connect_non_completed_badge_challenges_url = (
|
|
54
|
+
"/badgechallenge-service/badgeChallenge/non-completed"
|
|
55
|
+
)
|
|
56
|
+
self.garmin_connect_inprogress_virtual_challenges_url = (
|
|
57
|
+
"/badgechallenge-service/virtualChallenge/inProgress"
|
|
58
|
+
)
|
|
59
|
+
self.garmin_connect_daily_sleep_url = (
|
|
60
|
+
"/wellness-service/wellness/dailySleepData"
|
|
61
|
+
)
|
|
62
|
+
self.garmin_connect_daily_stress_url = (
|
|
63
|
+
"/wellness-service/wellness/dailyStress"
|
|
64
|
+
)
|
|
65
|
+
self.garmin_connect_hill_score_url = (
|
|
66
|
+
"/metrics-service/metrics/hillscore"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.garmin_connect_daily_body_battery_url = (
|
|
70
|
+
"/wellness-service/wellness/bodyBattery/reports/daily"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.garmin_connect_blood_pressure_endpoint = (
|
|
74
|
+
"/bloodpressure-service/bloodpressure/range"
|
|
75
|
+
)
|
|
76
|
+
self.garmin_connect_endurance_score_url = (
|
|
77
|
+
"/metrics-service/metrics/endurancescore"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.garmin_connect_goals_url = "/goal-service/goal/goals"
|
|
81
|
+
|
|
82
|
+
self.garmin_connect_rhr_url = "/userstats-service/wellness/daily"
|
|
83
|
+
|
|
84
|
+
self.garmin_connect_hrv_url = "/hrv-service/hrv"
|
|
85
|
+
|
|
86
|
+
self.garmin_connect_training_readiness_url = (
|
|
87
|
+
"/metrics-service/metrics/trainingreadiness"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self.garmin_connect_training_status_url = (
|
|
91
|
+
"/metrics-service/metrics/trainingstatus/aggregated"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self.garmin_connect_user_summary_chart = (
|
|
95
|
+
"/wellness-service/wellness/dailySummaryChart"
|
|
96
|
+
)
|
|
97
|
+
self.garmin_connect_floors_chart_daily_url = (
|
|
98
|
+
"/wellness-service/wellness/floorsChartData/daily"
|
|
99
|
+
)
|
|
100
|
+
self.garmin_connect_heartrates_daily_url = (
|
|
101
|
+
"/wellness-service/wellness/dailyHeartRate"
|
|
102
|
+
)
|
|
103
|
+
self.garmin_connect_daily_respiration_url = (
|
|
104
|
+
"/wellness-service/wellness/daily/respiration"
|
|
105
|
+
)
|
|
106
|
+
self.garmin_connect_daily_spo2_url = (
|
|
107
|
+
"/wellness-service/wellness/daily/spo2"
|
|
108
|
+
)
|
|
109
|
+
self.garmin_connect_activities = (
|
|
110
|
+
"/activitylist-service/activities/search/activities"
|
|
111
|
+
)
|
|
112
|
+
self.garmin_connect_activity = "/activity-service/activity"
|
|
113
|
+
self.garmin_connect_activity_types = (
|
|
114
|
+
"/activity-service/activity/activityTypes"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self.garmin_connect_fitnessstats = "/fitnessstats-service/activity"
|
|
118
|
+
|
|
119
|
+
self.garmin_connect_fit_download = "/download-service/files/activity"
|
|
120
|
+
self.garmin_connect_tcx_download = (
|
|
121
|
+
"/download-service/export/tcx/activity"
|
|
122
|
+
)
|
|
123
|
+
self.garmin_connect_gpx_download = (
|
|
124
|
+
"/download-service/export/gpx/activity"
|
|
125
|
+
)
|
|
126
|
+
self.garmin_connect_kml_download = (
|
|
127
|
+
"/download-service/export/kml/activity"
|
|
128
|
+
)
|
|
129
|
+
self.garmin_connect_csv_download = (
|
|
130
|
+
"/download-service/export/csv/activity"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.garmin_connect_upload = "/upload-service/upload"
|
|
134
|
+
|
|
135
|
+
self.garmin_connect_gear = "/gear-service/gear/filterGear"
|
|
136
|
+
self.garmin_connect_gear_baseurl = "/gear-service/gear/"
|
|
137
|
+
|
|
138
|
+
self.garmin_connect_logout = "auth/logout/?url="
|
|
139
|
+
|
|
140
|
+
self.garth = garth.Client(
|
|
141
|
+
domain="garmin.cn" if is_cn else "garmin.com"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
self.display_name = None
|
|
145
|
+
self.full_name = None
|
|
146
|
+
self.unit_system = None
|
|
147
|
+
|
|
148
|
+
def connectapi(self, path, **kwargs):
|
|
149
|
+
return self.garth.connectapi(path, **kwargs)
|
|
150
|
+
|
|
151
|
+
def download(self, path, **kwargs):
|
|
152
|
+
return self.garth.download(path, **kwargs)
|
|
153
|
+
|
|
154
|
+
def login(self, /, tokenstore: Optional[str] = None):
|
|
155
|
+
"""Log in using Garth."""
|
|
156
|
+
tokenstore = tokenstore or os.getenv("GARMINTOKENS")
|
|
157
|
+
|
|
158
|
+
if tokenstore:
|
|
159
|
+
self.garth.load(tokenstore)
|
|
160
|
+
else:
|
|
161
|
+
self.garth.login(self.username, self.password)
|
|
162
|
+
|
|
163
|
+
self.display_name = self.garth.profile["displayName"]
|
|
164
|
+
self.full_name = self.garth.profile["fullName"]
|
|
165
|
+
|
|
166
|
+
settings = self.garth.connectapi(
|
|
167
|
+
"/userprofile-service/userprofile/user-settings"
|
|
168
|
+
)
|
|
169
|
+
self.unit_system = settings["userData"]["measurementSystem"]
|
|
170
|
+
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def get_full_name(self):
|
|
174
|
+
"""Return full name."""
|
|
175
|
+
|
|
176
|
+
return self.full_name
|
|
177
|
+
|
|
178
|
+
def get_unit_system(self):
|
|
179
|
+
"""Return unit system."""
|
|
180
|
+
|
|
181
|
+
return self.unit_system
|
|
182
|
+
|
|
183
|
+
def get_stats(self, cdate: str) -> Dict[str, Any]:
|
|
184
|
+
"""
|
|
185
|
+
Return user activity summary for 'cdate' format 'YYYY-MM-DD'
|
|
186
|
+
(compat for garminconnect).
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
return self.get_user_summary(cdate)
|
|
190
|
+
|
|
191
|
+
def get_user_summary(self, cdate: str) -> Dict[str, Any]:
|
|
192
|
+
"""Return user activity summary for 'cdate' format 'YYYY-MM-DD'."""
|
|
193
|
+
|
|
194
|
+
url = f"{self.garmin_connect_daily_summary_url}/{self.display_name}"
|
|
195
|
+
params = {"calendarDate": str(cdate)}
|
|
196
|
+
logger.debug("Requesting user summary")
|
|
197
|
+
|
|
198
|
+
response = self.connectapi(url, params=params)
|
|
199
|
+
|
|
200
|
+
if response["privacyProtected"] is True:
|
|
201
|
+
raise GarminConnectAuthenticationError("Authentication error")
|
|
202
|
+
|
|
203
|
+
return response
|
|
204
|
+
|
|
205
|
+
def get_steps_data(self, cdate):
|
|
206
|
+
"""Fetch available steps data 'cDate' format 'YYYY-MM-DD'."""
|
|
207
|
+
|
|
208
|
+
url = f"{self.garmin_connect_user_summary_chart}/{self.display_name}"
|
|
209
|
+
params = {"date": str(cdate)}
|
|
210
|
+
logger.debug("Requesting steps data")
|
|
211
|
+
|
|
212
|
+
return self.connectapi(url, params=params)
|
|
213
|
+
|
|
214
|
+
def get_floors(self, cdate):
|
|
215
|
+
"""Fetch available floors data 'cDate' format 'YYYY-MM-DD'."""
|
|
216
|
+
|
|
217
|
+
url = f"{self.garmin_connect_floors_chart_daily_url}/{cdate}"
|
|
218
|
+
logger.debug("Requesting floors data")
|
|
219
|
+
|
|
220
|
+
return self.connectapi(url)
|
|
221
|
+
|
|
222
|
+
def get_daily_steps(self, start, end):
|
|
223
|
+
"""Fetch available steps data 'start' and 'end' format 'YYYY-MM-DD'."""
|
|
224
|
+
|
|
225
|
+
url = f"{self.garmin_connect_daily_stats_steps_url}/{start}/{end}"
|
|
226
|
+
logger.debug("Requesting daily steps data")
|
|
227
|
+
|
|
228
|
+
return self.connectapi(url)
|
|
229
|
+
|
|
230
|
+
def get_heart_rates(self, cdate):
|
|
231
|
+
"""Fetch available heart rates data 'cDate' format 'YYYY-MM-DD'."""
|
|
232
|
+
|
|
233
|
+
url = f"{self.garmin_connect_heartrates_daily_url}/{self.display_name}"
|
|
234
|
+
params = {"date": str(cdate)}
|
|
235
|
+
logger.debug("Requesting heart rates")
|
|
236
|
+
|
|
237
|
+
return self.connectapi(url, params=params)
|
|
238
|
+
|
|
239
|
+
def get_stats_and_body(self, cdate):
|
|
240
|
+
"""Return activity data and body composition (compat for garminconnect)."""
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
**self.get_stats(cdate),
|
|
244
|
+
**self.get_body_composition(cdate)["totalAverage"],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def get_body_composition(
|
|
248
|
+
self, startdate: str, enddate=None
|
|
249
|
+
) -> Dict[str, Any]:
|
|
250
|
+
"""
|
|
251
|
+
Return available body composition data for 'startdate' format
|
|
252
|
+
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
if enddate is None:
|
|
256
|
+
enddate = startdate
|
|
257
|
+
url = f"{self.garmin_connect_weight_url}/weight/dateRange"
|
|
258
|
+
params = {"startDate": str(startdate), "endDate": str(enddate)}
|
|
259
|
+
logger.debug("Requesting body composition")
|
|
260
|
+
|
|
261
|
+
return self.connectapi(url, params=params)
|
|
262
|
+
|
|
263
|
+
def add_weigh_in(
|
|
264
|
+
self, weight: int, unitKey: str = "kg", timestamp: str = ""
|
|
265
|
+
):
|
|
266
|
+
"""Add a weigh-in (default to kg)"""
|
|
267
|
+
|
|
268
|
+
url = f"{self.garmin_connect_weight_url}/user-weight"
|
|
269
|
+
dt = datetime.fromisoformat(timestamp) if timestamp else datetime.now()
|
|
270
|
+
# Apply timezone offset to get UTC/GMT time
|
|
271
|
+
dtGMT = dt - dt.astimezone().tzinfo.utcoffset(dt)
|
|
272
|
+
payload = {
|
|
273
|
+
"dateTimestamp": dt.isoformat()[:22] + ".00",
|
|
274
|
+
"gmtTimestamp": dtGMT.isoformat()[:22] + ".00",
|
|
275
|
+
"unitKey": unitKey,
|
|
276
|
+
"sourceType": "MANUAL",
|
|
277
|
+
"value": weight,
|
|
278
|
+
}
|
|
279
|
+
logger.debug("Adding weigh-in")
|
|
280
|
+
|
|
281
|
+
return self.garth.post("connectapi", url, json=payload)
|
|
282
|
+
|
|
283
|
+
def get_weigh_ins(self, startdate: str, enddate: str):
|
|
284
|
+
"""Get weigh-ins between startdate and enddate using format 'YYYY-MM-DD'."""
|
|
285
|
+
|
|
286
|
+
url = f"{self.garmin_connect_weight_url}/weight/range/{startdate}/{enddate}"
|
|
287
|
+
params = {"includeAll": True}
|
|
288
|
+
logger.debug("Requesting weigh-ins")
|
|
289
|
+
|
|
290
|
+
return self.connectapi(url, params=params)
|
|
291
|
+
|
|
292
|
+
def get_daily_weigh_ins(self, cdate: str):
|
|
293
|
+
"""Get weigh-ins for 'cdate' format 'YYYY-MM-DD'."""
|
|
294
|
+
|
|
295
|
+
url = f"{self.garmin_connect_weight_url}/weight/dayview/{cdate}"
|
|
296
|
+
params = {"includeAll": True}
|
|
297
|
+
logger.debug("Requesting weigh-ins")
|
|
298
|
+
|
|
299
|
+
return self.connectapi(url, params=params)
|
|
300
|
+
|
|
301
|
+
def delete_weigh_in(self, weight_pk: str, cdate: str):
|
|
302
|
+
"""Delete specific weigh-in."""
|
|
303
|
+
url = f"{self.garmin_connect_weight_url}/weight/{cdate}/byversion/{weight_pk}"
|
|
304
|
+
logger.debug("Deleting weigh-in")
|
|
305
|
+
|
|
306
|
+
return self.garth.request(
|
|
307
|
+
"DELETE",
|
|
308
|
+
"connectapi",
|
|
309
|
+
url,
|
|
310
|
+
api=True,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# return self.garth.post(
|
|
314
|
+
# "connectapi", url, {"x-http-method-override": "DELETE"}
|
|
315
|
+
# )
|
|
316
|
+
|
|
317
|
+
def delete_weigh_ins(self, cdate: str, delete_all: bool = False):
|
|
318
|
+
"""
|
|
319
|
+
Delete weigh-in for 'cdate' format 'YYYY-MM-DD'.
|
|
320
|
+
Includes option to delete all weigh-ins for that date.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
daily_weigh_ins = self.get_daily_weigh_ins(cdate)
|
|
324
|
+
weigh_ins = daily_weigh_ins.get("dateWeightList", [])
|
|
325
|
+
if not weigh_ins or len(weigh_ins) == 0:
|
|
326
|
+
logger.warning(f"No weigh-ins found on {cdate}")
|
|
327
|
+
return
|
|
328
|
+
elif len(weigh_ins) > 1:
|
|
329
|
+
logger.warning(f"Multiple weigh-ins found for {cdate}")
|
|
330
|
+
if not delete_all:
|
|
331
|
+
logger.warning(
|
|
332
|
+
f"Set delete_all to True to delete all {len(weigh_ins)} weigh-ins"
|
|
333
|
+
)
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
for w in weigh_ins:
|
|
337
|
+
self.delete_weigh_in(w["samplePk"], cdate)
|
|
338
|
+
|
|
339
|
+
return len(weigh_ins)
|
|
340
|
+
|
|
341
|
+
def get_body_battery(
|
|
342
|
+
self, startdate: str, enddate=None
|
|
343
|
+
) -> List[Dict[str, Any]]:
|
|
344
|
+
"""
|
|
345
|
+
Return body battery values by day for 'startdate' format
|
|
346
|
+
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
if enddate is None:
|
|
350
|
+
enddate = startdate
|
|
351
|
+
url = self.garmin_connect_daily_body_battery_url
|
|
352
|
+
params = {"startDate": str(startdate), "endDate": str(enddate)}
|
|
353
|
+
logger.debug("Requesting body battery data")
|
|
354
|
+
|
|
355
|
+
return self.connectapi(url, params=params)
|
|
356
|
+
|
|
357
|
+
def get_blood_pressure(
|
|
358
|
+
self, startdate: str, enddate=None
|
|
359
|
+
) -> Dict[str, Any]:
|
|
360
|
+
"""
|
|
361
|
+
Returns blood pressure by day for 'startdate' format
|
|
362
|
+
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
if enddate is None:
|
|
366
|
+
enddate = startdate
|
|
367
|
+
url = f"{self.garmin_connect_blood_pressure_endpoint}/{startdate}/{enddate}"
|
|
368
|
+
params = {"includeAll": True}
|
|
369
|
+
logger.debug("Requesting blood pressure data")
|
|
370
|
+
|
|
371
|
+
return self.connectapi(url, params=params)
|
|
372
|
+
|
|
373
|
+
def get_max_metrics(self, cdate: str) -> Dict[str, Any]:
|
|
374
|
+
"""Return available max metric data for 'cdate' format 'YYYY-MM-DD'."""
|
|
375
|
+
|
|
376
|
+
url = f"{self.garmin_connect_metrics_url}/{cdate}/{cdate}"
|
|
377
|
+
logger.debug("Requesting max metrics")
|
|
378
|
+
|
|
379
|
+
return self.connectapi(url)
|
|
380
|
+
|
|
381
|
+
def get_hydration_data(self, cdate: str) -> Dict[str, Any]:
|
|
382
|
+
"""Return available hydration data 'cdate' format 'YYYY-MM-DD'."""
|
|
383
|
+
|
|
384
|
+
url = f"{self.garmin_connect_daily_hydration_url}/{cdate}"
|
|
385
|
+
logger.debug("Requesting hydration data")
|
|
386
|
+
|
|
387
|
+
return self.connectapi(url)
|
|
388
|
+
|
|
389
|
+
def get_respiration_data(self, cdate: str) -> Dict[str, Any]:
|
|
390
|
+
"""Return available respiration data 'cdate' format 'YYYY-MM-DD'."""
|
|
391
|
+
|
|
392
|
+
url = f"{self.garmin_connect_daily_respiration_url}/{cdate}"
|
|
393
|
+
logger.debug("Requesting respiration data")
|
|
394
|
+
|
|
395
|
+
return self.connectapi(url)
|
|
396
|
+
|
|
397
|
+
def get_spo2_data(self, cdate: str) -> Dict[str, Any]:
|
|
398
|
+
"""Return available SpO2 data 'cdate' format 'YYYY-MM-DD'."""
|
|
399
|
+
|
|
400
|
+
url = f"{self.garmin_connect_daily_spo2_url}/{cdate}"
|
|
401
|
+
logger.debug("Requesting SpO2 data")
|
|
402
|
+
|
|
403
|
+
return self.connectapi(url)
|
|
404
|
+
|
|
405
|
+
def get_personal_record(self) -> Dict[str, Any]:
|
|
406
|
+
"""Return personal records for current user."""
|
|
407
|
+
|
|
408
|
+
url = f"{self.garmin_connect_personal_record_url}/{self.display_name}"
|
|
409
|
+
logger.debug("Requesting personal records for user")
|
|
410
|
+
|
|
411
|
+
return self.connectapi(url)
|
|
412
|
+
|
|
413
|
+
def get_earned_badges(self) -> Dict[str, Any]:
|
|
414
|
+
"""Return earned badges for current user."""
|
|
415
|
+
|
|
416
|
+
url = self.garmin_connect_earned_badges_url
|
|
417
|
+
logger.debug("Requesting earned badges for user")
|
|
418
|
+
|
|
419
|
+
return self.connectapi(url)
|
|
420
|
+
|
|
421
|
+
def get_adhoc_challenges(self, start, limit) -> Dict[str, Any]:
|
|
422
|
+
"""Return adhoc challenges for current user."""
|
|
423
|
+
|
|
424
|
+
url = self.garmin_connect_adhoc_challenges_url
|
|
425
|
+
params = {"start": str(start), "limit": str(limit)}
|
|
426
|
+
logger.debug("Requesting adhoc challenges for user")
|
|
427
|
+
|
|
428
|
+
return self.connectapi(url, params=params)
|
|
429
|
+
|
|
430
|
+
def get_badge_challenges(self, start, limit) -> Dict[str, Any]:
|
|
431
|
+
"""Return badge challenges for current user."""
|
|
432
|
+
|
|
433
|
+
url = self.garmin_connect_badge_challenges_url
|
|
434
|
+
params = {"start": str(start), "limit": str(limit)}
|
|
435
|
+
logger.debug("Requesting badge challenges for user")
|
|
436
|
+
|
|
437
|
+
return self.connectapi(url, params=params)
|
|
438
|
+
|
|
439
|
+
def get_available_badge_challenges(self, start, limit) -> Dict[str, Any]:
|
|
440
|
+
"""Return available badge challenges."""
|
|
441
|
+
|
|
442
|
+
url = self.garmin_connect_available_badge_challenges_url
|
|
443
|
+
params = {"start": str(start), "limit": str(limit)}
|
|
444
|
+
logger.debug("Requesting available badge challenges")
|
|
445
|
+
|
|
446
|
+
return self.connectapi(url, params=params)
|
|
447
|
+
|
|
448
|
+
def get_non_completed_badge_challenges(
|
|
449
|
+
self, start, limit
|
|
450
|
+
) -> Dict[str, Any]:
|
|
451
|
+
"""Return badge non-completed challenges for current user."""
|
|
452
|
+
|
|
453
|
+
url = self.garmin_connect_non_completed_badge_challenges_url
|
|
454
|
+
params = {"start": str(start), "limit": str(limit)}
|
|
455
|
+
logger.debug("Requesting badge challenges for user")
|
|
456
|
+
|
|
457
|
+
return self.connectapi(url, params=params)
|
|
458
|
+
|
|
459
|
+
def get_inprogress_virtual_challenges(
|
|
460
|
+
self, start, limit
|
|
461
|
+
) -> Dict[str, Any]:
|
|
462
|
+
"""Return in-progress virtual challenges for current user."""
|
|
463
|
+
|
|
464
|
+
url = self.garmin_connect_inprogress_virtual_challenges_url
|
|
465
|
+
params = {"start": str(start), "limit": str(limit)}
|
|
466
|
+
logger.debug("Requesting in-progress virtual challenges for user")
|
|
467
|
+
|
|
468
|
+
return self.connectapi(url, params=params)
|
|
469
|
+
|
|
470
|
+
def get_sleep_data(self, cdate: str) -> Dict[str, Any]:
|
|
471
|
+
"""Return sleep data for current user."""
|
|
472
|
+
|
|
473
|
+
url = f"{self.garmin_connect_daily_sleep_url}/{self.display_name}"
|
|
474
|
+
params = {"date": str(cdate), "nonSleepBufferMinutes": 60}
|
|
475
|
+
logger.debug("Requesting sleep data")
|
|
476
|
+
|
|
477
|
+
return self.connectapi(url, params=params)
|
|
478
|
+
|
|
479
|
+
def get_stress_data(self, cdate: str) -> Dict[str, Any]:
|
|
480
|
+
"""Return stress data for current user."""
|
|
481
|
+
|
|
482
|
+
url = f"{self.garmin_connect_daily_stress_url}/{cdate}"
|
|
483
|
+
logger.debug("Requesting stress data")
|
|
484
|
+
|
|
485
|
+
return self.connectapi(url)
|
|
486
|
+
|
|
487
|
+
def get_rhr_day(self, cdate: str) -> Dict[str, Any]:
|
|
488
|
+
"""Return resting heartrate data for current user."""
|
|
489
|
+
|
|
490
|
+
url = f"{self.garmin_connect_rhr_url}/{self.display_name}"
|
|
491
|
+
params = {
|
|
492
|
+
"fromDate": str(cdate),
|
|
493
|
+
"untilDate": str(cdate),
|
|
494
|
+
"metricId": 60,
|
|
495
|
+
}
|
|
496
|
+
logger.debug("Requesting resting heartrate data")
|
|
497
|
+
|
|
498
|
+
return self.connectapi(url, params=params)
|
|
499
|
+
|
|
500
|
+
def get_hrv_data(self, cdate: str) -> Dict[str, Any]:
|
|
501
|
+
"""Return Heart Rate Variability (hrv) data for current user."""
|
|
502
|
+
|
|
503
|
+
url = f"{self.garmin_connect_hrv_url}/{cdate}"
|
|
504
|
+
logger.debug("Requesting Heart Rate Variability (hrv) data")
|
|
505
|
+
|
|
506
|
+
return self.connectapi(url)
|
|
507
|
+
|
|
508
|
+
def get_training_readiness(self, cdate: str) -> Dict[str, Any]:
|
|
509
|
+
"""Return training readiness data for current user."""
|
|
510
|
+
|
|
511
|
+
url = f"{self.garmin_connect_training_readiness_url}/{cdate}"
|
|
512
|
+
logger.debug("Requesting training readiness data")
|
|
513
|
+
|
|
514
|
+
return self.connectapi(url)
|
|
515
|
+
|
|
516
|
+
def get_endurance_score(self, startdate: str, enddate=None):
|
|
517
|
+
"""
|
|
518
|
+
Return endurance score by day for 'startdate' format 'YYYY-MM-DD'
|
|
519
|
+
through enddate 'YYYY-MM-DD'.
|
|
520
|
+
Using a single day returns the precise values for that day.
|
|
521
|
+
Using a range returns the aggregated weekly values for that week.
|
|
522
|
+
"""
|
|
523
|
+
|
|
524
|
+
if enddate is None:
|
|
525
|
+
url = self.garmin_connect_endurance_score_url
|
|
526
|
+
params = {"calendarDate": str(startdate)}
|
|
527
|
+
logger.debug("Requesting endurance score data for a single day")
|
|
528
|
+
|
|
529
|
+
return self.connectapi(url, params=params)
|
|
530
|
+
else:
|
|
531
|
+
url = f"{self.garmin_connect_endurance_score_url}/stats"
|
|
532
|
+
params = {
|
|
533
|
+
"startDate": str(startdate),
|
|
534
|
+
"endDate": str(enddate),
|
|
535
|
+
"aggregation": "weekly",
|
|
536
|
+
}
|
|
537
|
+
logger.debug("Requesting endurance score data for a range of days")
|
|
538
|
+
|
|
539
|
+
return self.connectapi(url, params=params)
|
|
540
|
+
|
|
541
|
+
def get_training_status(self, cdate: str) -> Dict[str, Any]:
|
|
542
|
+
"""Return training status data for current user."""
|
|
543
|
+
|
|
544
|
+
url = f"{self.garmin_connect_training_status_url}/{cdate}"
|
|
545
|
+
logger.debug("Requesting training status data")
|
|
546
|
+
|
|
547
|
+
return self.connectapi(url)
|
|
548
|
+
|
|
549
|
+
def get_hill_score(self, startdate: str, enddate=None):
|
|
550
|
+
"""
|
|
551
|
+
Return hill score by day from 'startdate' format 'YYYY-MM-DD'
|
|
552
|
+
to enddate 'YYYY-MM-DD'
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
if enddate is None:
|
|
556
|
+
url = self.garmin_connect_hill_score_url
|
|
557
|
+
params = {"calendarDate": str(startdate)}
|
|
558
|
+
logger.debug("Requesting hill score data for a single day")
|
|
559
|
+
|
|
560
|
+
return self.connectapi(url, params=params)
|
|
561
|
+
|
|
562
|
+
else:
|
|
563
|
+
url = f"{self.garmin_connect_hill_score_url}/stats"
|
|
564
|
+
params = {
|
|
565
|
+
"startDate": str(startdate),
|
|
566
|
+
"endDate": str(enddate),
|
|
567
|
+
"aggregation": "daily",
|
|
568
|
+
}
|
|
569
|
+
logger.debug("Requesting hill score data for a range of days")
|
|
570
|
+
|
|
571
|
+
return self.connectapi(url, params=params)
|
|
572
|
+
|
|
573
|
+
def get_devices(self) -> Dict[str, Any]:
|
|
574
|
+
"""Return available devices for the current user account."""
|
|
575
|
+
|
|
576
|
+
url = self.garmin_connect_devices_url
|
|
577
|
+
logger.debug("Requesting devices")
|
|
578
|
+
|
|
579
|
+
return self.connectapi(url)
|
|
580
|
+
|
|
581
|
+
def get_device_settings(self, device_id: str) -> Dict[str, Any]:
|
|
582
|
+
"""Return device settings for device with 'device_id'."""
|
|
583
|
+
|
|
584
|
+
url = f"{self.garmin_connect_device_url}/device-info/settings/{device_id}"
|
|
585
|
+
logger.debug("Requesting device settings")
|
|
586
|
+
|
|
587
|
+
return self.connectapi(url)
|
|
588
|
+
|
|
589
|
+
def get_device_alarms(self) -> Dict[str, Any]:
|
|
590
|
+
"""Get list of active alarms from all devices."""
|
|
591
|
+
|
|
592
|
+
logger.debug("Requesting device alarms")
|
|
593
|
+
|
|
594
|
+
alarms = []
|
|
595
|
+
devices = self.get_devices()
|
|
596
|
+
for device in devices:
|
|
597
|
+
device_settings = self.get_device_settings(device["deviceId"])
|
|
598
|
+
device_alarms = device_settings["alarms"]
|
|
599
|
+
if device_alarms is not None:
|
|
600
|
+
alarms += device_alarms
|
|
601
|
+
return alarms
|
|
602
|
+
|
|
603
|
+
def get_device_last_used(self):
|
|
604
|
+
"""Return device last used."""
|
|
605
|
+
|
|
606
|
+
url = f"{self.garmin_connect_device_url}/mylastused"
|
|
607
|
+
logger.debug("Requesting device last used")
|
|
608
|
+
|
|
609
|
+
return self.connectapi(url)
|
|
610
|
+
|
|
611
|
+
def get_activities(self, start, limit):
|
|
612
|
+
"""Return available activities."""
|
|
613
|
+
|
|
614
|
+
url = self.garmin_connect_activities
|
|
615
|
+
params = {"start": str(start), "limit": str(limit)}
|
|
616
|
+
logger.debug("Requesting activities")
|
|
617
|
+
|
|
618
|
+
return self.connectapi(url, params=params)
|
|
619
|
+
|
|
620
|
+
def get_last_activity(self):
|
|
621
|
+
"""Return last activity."""
|
|
622
|
+
|
|
623
|
+
activities = self.get_activities(0, 1)
|
|
624
|
+
if activities:
|
|
625
|
+
return activities[-1]
|
|
626
|
+
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
def upload_activity(self, activity_path: str):
|
|
630
|
+
"""Upload activity in fit format from file."""
|
|
631
|
+
# This code is borrowed from python-garminconnect-enhanced ;-)
|
|
632
|
+
|
|
633
|
+
file_base_name = os.path.basename(activity_path)
|
|
634
|
+
file_extension = file_base_name.split(".")[-1]
|
|
635
|
+
allowed_file_extension = (
|
|
636
|
+
file_extension.upper() in Garmin.ActivityUploadFormat.__members__
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if allowed_file_extension:
|
|
640
|
+
files = {
|
|
641
|
+
"file": (file_base_name, open(activity_path, "rb" or "r")),
|
|
642
|
+
}
|
|
643
|
+
url = self.garmin_connect_upload
|
|
644
|
+
return self.garth.post("connectapi", url, files=files)
|
|
645
|
+
else:
|
|
646
|
+
raise GarminConnectInvalidFileFormatError(
|
|
647
|
+
f"Could not upload {activity_path}"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
def get_activities_by_date(self, startdate, enddate, activitytype=None):
|
|
651
|
+
"""
|
|
652
|
+
Fetch available activities between specific dates
|
|
653
|
+
:param startdate: String in the format YYYY-MM-DD
|
|
654
|
+
:param enddate: String in the format YYYY-MM-DD
|
|
655
|
+
:param activitytype: (Optional) Type of activity you are searching
|
|
656
|
+
Possible values are [cycling, running, swimming,
|
|
657
|
+
multi_sport, fitness_equipment, hiking, walking, other]
|
|
658
|
+
:return: list of JSON activities
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
activities = []
|
|
662
|
+
start = 0
|
|
663
|
+
limit = 20
|
|
664
|
+
# mimicking the behavior of the web interface that fetches
|
|
665
|
+
# 20 activities at a time
|
|
666
|
+
# and automatically loads more on scroll
|
|
667
|
+
url = self.garmin_connect_activities
|
|
668
|
+
params = {
|
|
669
|
+
"startDate": str(startdate),
|
|
670
|
+
"endDate": str(enddate),
|
|
671
|
+
"start": str(start),
|
|
672
|
+
"limit": str(limit),
|
|
673
|
+
}
|
|
674
|
+
if activitytype:
|
|
675
|
+
params["activityType"] = str(activitytype)
|
|
676
|
+
|
|
677
|
+
logger.debug(
|
|
678
|
+
f"Requesting activities by date from {startdate} to {enddate}"
|
|
679
|
+
)
|
|
680
|
+
while True:
|
|
681
|
+
params["start"] = str(start)
|
|
682
|
+
logger.debug(f"Requesting activities {start} to {start+limit}")
|
|
683
|
+
act = self.connectapi(url, params=params)
|
|
684
|
+
if act:
|
|
685
|
+
activities.extend(act)
|
|
686
|
+
start = start + limit
|
|
687
|
+
else:
|
|
688
|
+
break
|
|
689
|
+
|
|
690
|
+
return activities
|
|
691
|
+
|
|
692
|
+
def get_progress_summary_between_dates(
|
|
693
|
+
self, startdate, enddate, metric="distance"
|
|
694
|
+
):
|
|
695
|
+
"""
|
|
696
|
+
Fetch progress summary data between specific dates
|
|
697
|
+
:param startdate: String in the format YYYY-MM-DD
|
|
698
|
+
:param enddate: String in the format YYYY-MM-DD
|
|
699
|
+
:param metric: metric to be calculated in the summary:
|
|
700
|
+
"elevationGain", "duration", "distance", "movingDuration"
|
|
701
|
+
:return: list of JSON activities with their aggregated progress summary
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
url = self.garmin_connect_fitnessstats
|
|
705
|
+
params = {
|
|
706
|
+
"startDate": str(startdate),
|
|
707
|
+
"endDate": str(enddate),
|
|
708
|
+
"aggregation": "lifetime",
|
|
709
|
+
"groupByParentActivityType": "true",
|
|
710
|
+
"metric": str(metric),
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
logger.debug(
|
|
714
|
+
f"Requesting fitnessstats by date from {startdate} to {enddate}"
|
|
715
|
+
)
|
|
716
|
+
return self.connectapi(url, params=params)
|
|
717
|
+
|
|
718
|
+
def get_activity_types(self):
|
|
719
|
+
url = self.garmin_connect_activity_types
|
|
720
|
+
logger.debug("Requesting activy types")
|
|
721
|
+
return self.connectapi(url)
|
|
722
|
+
|
|
723
|
+
def get_goals(self, status="active", start=1, limit=30):
|
|
724
|
+
"""
|
|
725
|
+
Fetch all goals based on status
|
|
726
|
+
:param status: Status of goals (valid options are "active", "future", or "past")
|
|
727
|
+
:type status: str
|
|
728
|
+
:param start: Initial goal index
|
|
729
|
+
:type start: int
|
|
730
|
+
:param limit: Pagination limit when retrieving goals
|
|
731
|
+
:type limit: int
|
|
732
|
+
:return: list of goals in JSON format
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
goals = []
|
|
736
|
+
url = self.garmin_connect_goals_url
|
|
737
|
+
params = {
|
|
738
|
+
"status": status,
|
|
739
|
+
"start": str(start),
|
|
740
|
+
"limit": str(limit),
|
|
741
|
+
"sortOrder": "asc",
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
logger.debug(f"Requesting {status} goals")
|
|
745
|
+
while True:
|
|
746
|
+
params["start"] = str(start)
|
|
747
|
+
logger.debug(
|
|
748
|
+
f"Requesting {status} goals {start} to {start + limit - 1}"
|
|
749
|
+
)
|
|
750
|
+
goals_json = self.connectapi(url, params=params)
|
|
751
|
+
if goals_json:
|
|
752
|
+
goals.extend(goals_json)
|
|
753
|
+
start = start + limit
|
|
754
|
+
else:
|
|
755
|
+
break
|
|
756
|
+
|
|
757
|
+
return goals
|
|
758
|
+
|
|
759
|
+
def get_gear(self, userProfileNumber):
|
|
760
|
+
"""Return all user gear."""
|
|
761
|
+
url = f"{self.garmin_connect_gear}?userProfilePk={userProfileNumber}"
|
|
762
|
+
logger.debug("Requesting gear for user %s", userProfileNumber)
|
|
763
|
+
|
|
764
|
+
return self.connectapi(url)
|
|
765
|
+
|
|
766
|
+
def get_gear_stats(self, gearUUID):
|
|
767
|
+
url = f"{self.garmin_connect_gear_baseurl}stats/{gearUUID}"
|
|
768
|
+
logger.debug("Requesting gear stats for gearUUID %s", gearUUID)
|
|
769
|
+
return self.connectapi(url)
|
|
770
|
+
|
|
771
|
+
def get_gear_defaults(self, userProfileNumber):
|
|
772
|
+
url = (
|
|
773
|
+
f"{self.garmin_connect_gear_baseurl}user/"
|
|
774
|
+
f"{userProfileNumber}/activityTypes"
|
|
775
|
+
)
|
|
776
|
+
logger.debug("Requesting gear for user %s", userProfileNumber)
|
|
777
|
+
return self.connectapi(url)
|
|
778
|
+
|
|
779
|
+
def set_gear_default(self, activityType, gearUUID, defaultGear=True):
|
|
780
|
+
defaultGearString = "/default/true" if defaultGear else ""
|
|
781
|
+
method_override = "PUT" if defaultGear else "DELETE"
|
|
782
|
+
url = (
|
|
783
|
+
f"{self.garmin_connect_gear_baseurl}{gearUUID}/"
|
|
784
|
+
f"activityType/{activityType}{defaultGearString}"
|
|
785
|
+
)
|
|
786
|
+
return self.garth.post(
|
|
787
|
+
"connectapi", url, {"x-http-method-override": method_override}
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
class ActivityDownloadFormat(Enum):
|
|
791
|
+
"""Activity variables."""
|
|
792
|
+
|
|
793
|
+
ORIGINAL = auto()
|
|
794
|
+
TCX = auto()
|
|
795
|
+
GPX = auto()
|
|
796
|
+
KML = auto()
|
|
797
|
+
CSV = auto()
|
|
798
|
+
|
|
799
|
+
class ActivityUploadFormat(Enum):
|
|
800
|
+
FIT = auto()
|
|
801
|
+
GPX = auto()
|
|
802
|
+
TCX = auto()
|
|
803
|
+
|
|
804
|
+
def download_activity(
|
|
805
|
+
self, activity_id, dl_fmt=ActivityDownloadFormat.TCX
|
|
806
|
+
):
|
|
807
|
+
"""
|
|
808
|
+
Downloads activity in requested format and returns the raw bytes. For
|
|
809
|
+
"Original" will return the zip file content, up to user to extract it.
|
|
810
|
+
"CSV" will return a csv of the splits.
|
|
811
|
+
"""
|
|
812
|
+
activity_id = str(activity_id)
|
|
813
|
+
urls = {
|
|
814
|
+
Garmin.ActivityDownloadFormat.ORIGINAL: f"{self.garmin_connect_fit_download}/{activity_id}",
|
|
815
|
+
Garmin.ActivityDownloadFormat.TCX: f"{self.garmin_connect_tcx_download}/{activity_id}",
|
|
816
|
+
Garmin.ActivityDownloadFormat.GPX: f"{self.garmin_connect_gpx_download}/{activity_id}",
|
|
817
|
+
Garmin.ActivityDownloadFormat.KML: f"{self.garmin_connect_kml_download}/{activity_id}",
|
|
818
|
+
Garmin.ActivityDownloadFormat.CSV: f"{self.garmin_connect_csv_download}/{activity_id}",
|
|
819
|
+
}
|
|
820
|
+
if dl_fmt not in urls:
|
|
821
|
+
raise ValueError(f"Unexpected value {dl_fmt} for dl_fmt")
|
|
822
|
+
url = urls[dl_fmt]
|
|
823
|
+
|
|
824
|
+
logger.debug("Downloading activities from %s", url)
|
|
825
|
+
|
|
826
|
+
return self.download(url)
|
|
827
|
+
|
|
828
|
+
def get_activity_splits(self, activity_id):
|
|
829
|
+
"""Return activity splits."""
|
|
830
|
+
|
|
831
|
+
activity_id = str(activity_id)
|
|
832
|
+
url = f"{self.garmin_connect_activity}/{activity_id}/splits"
|
|
833
|
+
logger.debug("Requesting splits for activity id %s", activity_id)
|
|
834
|
+
|
|
835
|
+
return self.connectapi(url)
|
|
836
|
+
|
|
837
|
+
def get_activity_split_summaries(self, activity_id):
|
|
838
|
+
"""Return activity split summaries."""
|
|
839
|
+
|
|
840
|
+
activity_id = str(activity_id)
|
|
841
|
+
url = f"{self.garmin_connect_activity}/{activity_id}/split_summaries"
|
|
842
|
+
logger.debug(
|
|
843
|
+
"Requesting split summaries for activity id %s", activity_id
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
return self.connectapi(url)
|
|
847
|
+
|
|
848
|
+
def get_activity_weather(self, activity_id):
|
|
849
|
+
"""Return activity weather."""
|
|
850
|
+
|
|
851
|
+
activity_id = str(activity_id)
|
|
852
|
+
url = f"{self.garmin_connect_activity}/{activity_id}/weather"
|
|
853
|
+
logger.debug("Requesting weather for activity id %s", activity_id)
|
|
854
|
+
|
|
855
|
+
return self.connectapi(url)
|
|
856
|
+
|
|
857
|
+
def get_activity_hr_in_timezones(self, activity_id):
|
|
858
|
+
"""Return activity heartrate in timezones."""
|
|
859
|
+
|
|
860
|
+
activity_id = str(activity_id)
|
|
861
|
+
url = f"{self.garmin_connect_activity}/{activity_id}/hrTimeInZones"
|
|
862
|
+
logger.debug(
|
|
863
|
+
"Requesting split summaries for activity id %s", activity_id
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
return self.connectapi(url)
|
|
867
|
+
|
|
868
|
+
def get_activity_evaluation(self, activity_id):
|
|
869
|
+
"""Return activity self evaluation details."""
|
|
870
|
+
|
|
871
|
+
activity_id = str(activity_id)
|
|
872
|
+
url = f"{self.garmin_connect_activity}/{activity_id}"
|
|
873
|
+
logger.debug(
|
|
874
|
+
"Requesting self evaluation data for activity id %s", activity_id
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
return self.connectapi(url)
|
|
878
|
+
|
|
879
|
+
def get_activity_details(self, activity_id, maxchart=2000, maxpoly=4000):
|
|
880
|
+
"""Return activity details."""
|
|
881
|
+
|
|
882
|
+
activity_id = str(activity_id)
|
|
883
|
+
params = {
|
|
884
|
+
"maxChartSize": str(maxchart),
|
|
885
|
+
"maxPolylineSize": str(maxpoly),
|
|
886
|
+
}
|
|
887
|
+
url = f"{self.garmin_connect_activity}/{activity_id}/details"
|
|
888
|
+
logger.debug("Requesting details for activity id %s", activity_id)
|
|
889
|
+
|
|
890
|
+
return self.connectapi(url, params=params)
|
|
891
|
+
|
|
892
|
+
def get_activity_exercise_sets(self, activity_id):
|
|
893
|
+
"""Return activity exercise sets."""
|
|
894
|
+
|
|
895
|
+
activity_id = str(activity_id)
|
|
896
|
+
url = f"{self.garmin_connect_activity}/{activity_id}/exerciseSets"
|
|
897
|
+
logger.debug(
|
|
898
|
+
"Requesting exercise sets for activity id %s", activity_id
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
return self.connectapi(url)
|
|
902
|
+
|
|
903
|
+
def get_activity_gear(self, activity_id):
|
|
904
|
+
"""Return gears used for activity id."""
|
|
905
|
+
|
|
906
|
+
activity_id = str(activity_id)
|
|
907
|
+
params = {
|
|
908
|
+
"activityId": str(activity_id),
|
|
909
|
+
}
|
|
910
|
+
url = self.garmin_connect_gear
|
|
911
|
+
logger.debug("Requesting gear for activity_id %s", activity_id)
|
|
912
|
+
|
|
913
|
+
return self.connectapi(url, params=params)
|
|
914
|
+
|
|
915
|
+
def logout(self):
|
|
916
|
+
"""Log user out of session."""
|
|
917
|
+
|
|
918
|
+
self.connectapi(self.garmin_connect_logout)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
class GarminConnectConnectionError(Exception):
|
|
922
|
+
"""Raised when communication ended in error."""
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
class GarminConnectTooManyRequestsError(Exception):
|
|
926
|
+
"""Raised when rate limit is exceeded."""
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
class GarminConnectAuthenticationError(Exception):
|
|
930
|
+
"""Raised when authentication is failed."""
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
class GarminConnectInvalidFileFormatError(Exception):
|
|
934
|
+
"""Raised when an invalid file format is passed to upload."""
|
garminconnect/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: garminconnect
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Python 3 API wrapper for Garmin Connect
|
|
5
|
+
Keywords: garmin connect api garmin
|
|
6
|
+
Author-Email: Ron Klinkien <ron@cyberjunky.nl>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Project-URL: Homepage, https://github.com/cyberjunky/python-garminconnect
|
|
14
|
+
Project-URL: Bug tracker, https://github.com/cyberjunky/python-garminconnect/issues
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: garth>=0.4.23
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Python: Garmin Connect
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
[](https://www.paypal.me/cyberjunkynl/)
|
|
24
|
+
|
|
25
|
+
Python 3 API wrapper for Garmin Connect to get your statistics.
|
|
26
|
+
|
|
27
|
+
## NOTE: For developers using this package
|
|
28
|
+
From `version 0.2.1 onwards`, this package uses `garth` to authenticate and perform API calls.
|
|
29
|
+
This requires minor changes to your login code, look at the code in `example.py` or the `reference.ipynb` file how to do that.
|
|
30
|
+
It fixes a lot of stability issues, so it's well worth the effort!
|
|
31
|
+
|
|
32
|
+
## About
|
|
33
|
+
|
|
34
|
+
This package allows you to request garmin device, activity and health data from your Garmin Connect account.
|
|
35
|
+
See <https://connect.garmin.com/>
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip3 install garminconnect
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Authentication
|
|
44
|
+
|
|
45
|
+
The library uses the same authentication method as the app using [Garth](https://github.com/matin/garth).
|
|
46
|
+
The login credentials generated with Garth are valid for a year to avoid needing to login each time.
|
|
47
|
+
|
|
48
|
+
## Testing
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sudo apt install python3-pytest (some distros)
|
|
52
|
+
|
|
53
|
+
make install-test
|
|
54
|
+
make test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
The tests provide examples of how to use the library.
|
|
60
|
+
There is a Jupyter notebook called `reference.ipynb` provided [here](https://github.com/cyberjunky/python-garminconnect/blob/master/reference.ipynb).
|
|
61
|
+
And you can check out the `example.py` code you can find [here](https://raw.githubusercontent.com/cyberjunky/python-garminconnect/master/example.py), you can run it like so:
|
|
62
|
+
```
|
|
63
|
+
pip3 install -r requirements-dev.txt
|
|
64
|
+
./example.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Credits
|
|
68
|
+
|
|
69
|
+
:heart: Special thanks to all people contibuted, either by asking questions, reporting bugs, coming up with great ideas, or even by creating whole Pull Requests to add new features!
|
|
70
|
+
This project deserves more attention, but I'm struggling to free up time sometimes, so thank you for your patience too!
|
|
71
|
+
|
|
72
|
+
## Donations
|
|
73
|
+
|
|
74
|
+
[](https://www.paypal.me/cyberjunkynl/)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
garminconnect-0.2.2.dist-info/METADATA,sha256=_FUckDgViy0pBNV-mBGAE80E1F22AiolbcWOAtXOSO4,2780
|
|
2
|
+
garminconnect-0.2.2.dist-info/WHEEL,sha256=1D4d-_ODszlK6-qFybc5BLF-ZhZvee-uKVh2hLa3IIc,90
|
|
3
|
+
garminconnect-0.2.2.dist-info/licenses/LICENSE,sha256=jQMjDyLxbdDjqjfKlFTmV3GNjZfdn5QrSlqegiQw8m0,1074
|
|
4
|
+
garminconnect/__init__.py,sha256=sMEb8Qrx49KKQhuiqPDXGyNaQ-YJgPKwiToeJB_-qrs,32560
|
|
5
|
+
garminconnect/version.py,sha256=m6kyaNpwBcP1XYcqrelX2oS3PJuOnElOcRdBa9pEb8c,22
|
|
6
|
+
garminconnect-0.2.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2023 Ron Klinkien
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|