22
22
MAX_HYDRATION_ML = 10000 # 10 liters
23
23
DATE_FORMAT_REGEX = r"^\d{4}-\d{2}-\d{2}$"
24
24
DATE_FORMAT_STR = "%Y-%m-%d"
25
- TIMESTAMP_FORMAT_STR = "%Y-%m-%dT%H:%M:%S.%f"
26
25
VALID_WEIGHT_UNITS = {"kg" , "lbs" }
27
26
28
27
@@ -273,15 +272,18 @@ def connectapi(self, path: str, **kwargs: Any) -> Any:
273
272
try :
274
273
return self .garth .connectapi (path , ** kwargs )
275
274
except HTTPError as e :
276
- logger .error (f"API call failed for path '{ path } ': { e } " )
277
- if e .response .status_code == 401 :
275
+ status = getattr (getattr (e , "response" , None ), "status_code" , None )
276
+ logger .error ("API call failed for path '%s': %s (status=%s)" , path , e , status )
277
+ if status == 401 :
278
278
raise GarminConnectAuthenticationError (
279
279
f"Authentication failed: { e } "
280
280
) from e
281
- elif e . response . status_code == 429 :
281
+ elif status == 429 :
282
282
raise GarminConnectTooManyRequestsError (
283
283
f"Rate limit exceeded: { e } "
284
284
) from e
285
+ else :
286
+ raise GarminConnectConnectionError (f"HTTP error: { e } " ) from e
285
287
except Exception as e :
286
288
raise GarminConnectConnectionError (f"Connection error: { e } " ) from e
287
289
@@ -290,7 +292,12 @@ def download(self, path: str, **kwargs: Any) -> Any:
290
292
try :
291
293
return self .garth .download (path , ** kwargs )
292
294
except Exception as e :
293
- logger .error (f"Download failed for path '{ path } ': { e } " )
295
+ status = getattr (getattr (e , "response" , None ), "status_code" , None )
296
+ logger .error ("Download failed for path '%s': %s (status=%s)" , path , e , status )
297
+ if status == 401 :
298
+ raise GarminConnectAuthenticationError (f"Download error: { e } " ) from e
299
+ if status == 429 :
300
+ raise GarminConnectTooManyRequestsError (f"Download error: { e } " ) from e
294
301
raise GarminConnectConnectionError (f"Download error: { e } " ) from e
295
302
296
303
def login (self , / , tokenstore : str | None = None ) -> tuple [str | None , str | None ]:
@@ -366,7 +373,7 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
366
373
if isinstance (e , GarminConnectAuthenticationError ):
367
374
raise
368
375
else :
369
- logger .error ("Login failed" )
376
+ logger .exception ("Login failed" )
370
377
raise GarminConnectConnectionError (f"Login failed: { e } " ) from e
371
378
372
379
def resume_login (
@@ -623,6 +630,8 @@ def add_weigh_in_with_timestamps(
623
630
else dt .astimezone (timezone .utc )
624
631
)
625
632
633
+ # Validate weight for consistency with add_weigh_in
634
+ weight = _validate_positive_number (weight , "weight" )
626
635
# Build the payload
627
636
payload = {
628
637
"dateTimestamp" : dt .isoformat ()[:19 ] + ".00" , # Local time
@@ -641,6 +650,8 @@ def add_weigh_in_with_timestamps(
641
650
def get_weigh_ins (self , startdate : str , enddate : str ) -> dict [str , Any ]:
642
651
"""Get weigh-ins between startdate and enddate using format 'YYYY-MM-DD'."""
643
652
653
+ startdate = _validate_date_format (startdate , "startdate" )
654
+ enddate = _validate_date_format (enddate , "enddate" )
644
655
url = f"{ self .garmin_connect_weight_url } /weight/range/{ startdate } /{ enddate } "
645
656
params = {"includeAll" : True }
646
657
logger .debug ("Requesting weigh-ins" )
@@ -650,6 +661,7 @@ def get_weigh_ins(self, startdate: str, enddate: str) -> dict[str, Any]:
650
661
def get_daily_weigh_ins (self , cdate : str ) -> dict [str , Any ]:
651
662
"""Get weigh-ins for 'cdate' format 'YYYY-MM-DD'."""
652
663
664
+ cdate = _validate_date_format (cdate , "cdate" )
653
665
url = f"{ self .garmin_connect_weight_url } /weight/dayview/{ cdate } "
654
666
params = {"includeAll" : True }
655
667
logger .debug ("Requesting weigh-ins" )
@@ -700,8 +712,11 @@ def get_body_battery(
700
712
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
701
713
"""
702
714
715
+ startdate = _validate_date_format (startdate , "startdate" )
703
716
if enddate is None :
704
717
enddate = startdate
718
+ else :
719
+ enddate = _validate_date_format (enddate , "enddate" )
705
720
url = self .garmin_connect_daily_body_battery_url
706
721
params = {"startDate" : str (startdate ), "endDate" : str (enddate )}
707
722
logger .debug ("Requesting body battery data" )
@@ -759,8 +774,11 @@ def get_blood_pressure(
759
774
'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
760
775
"""
761
776
777
+ startdate = _validate_date_format (startdate , "startdate" )
762
778
if enddate is None :
763
779
enddate = startdate
780
+ else :
781
+ enddate = _validate_date_format (enddate , "enddate" )
764
782
url = f"{ self .garmin_connect_blood_pressure_endpoint } /{ startdate } /{ enddate } "
765
783
params = {"includeAll" : True }
766
784
logger .debug ("Requesting blood pressure data" )
@@ -958,8 +976,7 @@ def add_hydration_data(
958
976
}
959
977
960
978
logger .debug ("Adding hydration data" )
961
-
962
- return self .garth .put ("connectapi" , url , json = payload )
979
+ return self .garth .put ("connectapi" , url , json = payload ).json ()
963
980
964
981
def get_hydration_data (self , cdate : str ) -> dict [str , Any ]:
965
982
"""Return available hydration data 'cdate' format 'YYYY-MM-DD'."""
@@ -1232,13 +1249,12 @@ def get_race_predictions(
1232
1249
return self .connectapi (url )
1233
1250
1234
1251
elif _type is not None and startdate is not None and enddate is not None :
1235
- url = (
1236
- self .garmin_connect_race_predictor_url + f"/{ _type } /{ self .display_name } "
1237
- )
1238
- params = {
1239
- "fromCalendarDate" : str (startdate ),
1240
- "toCalendarDate" : str (enddate ),
1241
- }
1252
+ startdate = _validate_date_format (startdate , "startdate" )
1253
+ enddate = _validate_date_format (enddate , "enddate" )
1254
+ if (datetime .strptime (enddate , DATE_FORMAT_STR ).date () - datetime .strptime (startdate , DATE_FORMAT_STR ).date ()).days > 366 :
1255
+ raise ValueError ("Startdate cannot be more than one year before enddate" )
1256
+ url = self .garmin_connect_race_predictor_url + f"/{ _type } /{ self .display_name } "
1257
+ params = {"fromCalendarDate" : startdate , "toCalendarDate" : enddate }
1242
1258
return self .connectapi (url , params = params )
1243
1259
1244
1260
else :
@@ -1247,6 +1263,7 @@ def get_race_predictions(
1247
1263
def get_training_status (self , cdate : str ) -> dict [str , Any ]:
1248
1264
"""Return training status data for current user."""
1249
1265
1266
+ cdate = _validate_date_format (cdate , "cdate" )
1250
1267
url = f"{ self .garmin_connect_training_status_url } /{ cdate } "
1251
1268
logger .debug ("Requesting training status data" )
1252
1269
@@ -1255,6 +1272,7 @@ def get_training_status(self, cdate: str) -> dict[str, Any]:
1255
1272
def get_fitnessage_data (self , cdate : str ) -> dict [str , Any ]:
1256
1273
"""Return Fitness Age data for current user."""
1257
1274
1275
+ cdate = _validate_date_format (cdate , "cdate" )
1258
1276
url = f"{ self .garmin_connect_fitnessage } /{ cdate } "
1259
1277
logger .debug ("Requesting Fitness Age data" )
1260
1278
@@ -1573,13 +1591,16 @@ def get_activities_by_date(
1573
1591
# 20 activities at a time
1574
1592
# and automatically loads more on scroll
1575
1593
url = self .garmin_connect_activities
1594
+ startdate = _validate_date_format (startdate , "startdate" )
1595
+ if enddate is not None :
1596
+ enddate = _validate_date_format (enddate , "enddate" )
1576
1597
params = {
1577
- "startDate" : str ( startdate ) ,
1598
+ "startDate" : startdate ,
1578
1599
"start" : str (start ),
1579
1600
"limit" : str (limit ),
1580
1601
}
1581
1602
if enddate :
1582
- params ["endDate" ] = str ( enddate )
1603
+ params ["endDate" ] = enddate
1583
1604
if activitytype :
1584
1605
params ["activityType" ] = str (activitytype )
1585
1606
if sortorder :
@@ -1865,6 +1886,7 @@ def request_reload(self, cdate: str) -> dict[str, Any]:
1865
1886
Garmin offloads older data.
1866
1887
"""
1867
1888
1889
+ cdate = _validate_date_format (cdate , "cdate" )
1868
1890
url = f"{ self .garmin_request_reload_url } /{ cdate } "
1869
1891
logger .debug (f"Requesting reload of data for { cdate } ." )
1870
1892
0 commit comments