2222MAX_HYDRATION_ML = 10000 # 10 liters
2323DATE_FORMAT_REGEX = r"^\d{4}-\d{2}-\d{2}$"
2424DATE_FORMAT_STR = "%Y-%m-%d"
25- TIMESTAMP_FORMAT_STR = "%Y-%m-%dT%H:%M:%S.%f"
2625VALID_WEIGHT_UNITS = {"kg" , "lbs" }
2726
2827
@@ -273,15 +272,18 @@ def connectapi(self, path: str, **kwargs: Any) -> Any:
273272 try :
274273 return self .garth .connectapi (path , ** kwargs )
275274 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 :
278278 raise GarminConnectAuthenticationError (
279279 f"Authentication failed: { e } "
280280 ) from e
281- elif e . response . status_code == 429 :
281+ elif status == 429 :
282282 raise GarminConnectTooManyRequestsError (
283283 f"Rate limit exceeded: { e } "
284284 ) from e
285+ else :
286+ raise GarminConnectConnectionError (f"HTTP error: { e } " ) from e
285287 except Exception as e :
286288 raise GarminConnectConnectionError (f"Connection error: { e } " ) from e
287289
@@ -290,7 +292,12 @@ def download(self, path: str, **kwargs: Any) -> Any:
290292 try :
291293 return self .garth .download (path , ** kwargs )
292294 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
294301 raise GarminConnectConnectionError (f"Download error: { e } " ) from e
295302
296303 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
366373 if isinstance (e , GarminConnectAuthenticationError ):
367374 raise
368375 else :
369- logger .error ("Login failed" )
376+ logger .exception ("Login failed" )
370377 raise GarminConnectConnectionError (f"Login failed: { e } " ) from e
371378
372379 def resume_login (
@@ -623,6 +630,8 @@ def add_weigh_in_with_timestamps(
623630 else dt .astimezone (timezone .utc )
624631 )
625632
633+ # Validate weight for consistency with add_weigh_in
634+ weight = _validate_positive_number (weight , "weight" )
626635 # Build the payload
627636 payload = {
628637 "dateTimestamp" : dt .isoformat ()[:19 ] + ".00" , # Local time
@@ -641,6 +650,8 @@ def add_weigh_in_with_timestamps(
641650 def get_weigh_ins (self , startdate : str , enddate : str ) -> dict [str , Any ]:
642651 """Get weigh-ins between startdate and enddate using format 'YYYY-MM-DD'."""
643652
653+ startdate = _validate_date_format (startdate , "startdate" )
654+ enddate = _validate_date_format (enddate , "enddate" )
644655 url = f"{ self .garmin_connect_weight_url } /weight/range/{ startdate } /{ enddate } "
645656 params = {"includeAll" : True }
646657 logger .debug ("Requesting weigh-ins" )
@@ -650,6 +661,7 @@ def get_weigh_ins(self, startdate: str, enddate: str) -> dict[str, Any]:
650661 def get_daily_weigh_ins (self , cdate : str ) -> dict [str , Any ]:
651662 """Get weigh-ins for 'cdate' format 'YYYY-MM-DD'."""
652663
664+ cdate = _validate_date_format (cdate , "cdate" )
653665 url = f"{ self .garmin_connect_weight_url } /weight/dayview/{ cdate } "
654666 params = {"includeAll" : True }
655667 logger .debug ("Requesting weigh-ins" )
@@ -700,8 +712,11 @@ def get_body_battery(
700712 'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
701713 """
702714
715+ startdate = _validate_date_format (startdate , "startdate" )
703716 if enddate is None :
704717 enddate = startdate
718+ else :
719+ enddate = _validate_date_format (enddate , "enddate" )
705720 url = self .garmin_connect_daily_body_battery_url
706721 params = {"startDate" : str (startdate ), "endDate" : str (enddate )}
707722 logger .debug ("Requesting body battery data" )
@@ -759,8 +774,11 @@ def get_blood_pressure(
759774 'YYYY-MM-DD' through enddate 'YYYY-MM-DD'
760775 """
761776
777+ startdate = _validate_date_format (startdate , "startdate" )
762778 if enddate is None :
763779 enddate = startdate
780+ else :
781+ enddate = _validate_date_format (enddate , "enddate" )
764782 url = f"{ self .garmin_connect_blood_pressure_endpoint } /{ startdate } /{ enddate } "
765783 params = {"includeAll" : True }
766784 logger .debug ("Requesting blood pressure data" )
@@ -958,8 +976,7 @@ def add_hydration_data(
958976 }
959977
960978 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 ()
963980
964981 def get_hydration_data (self , cdate : str ) -> dict [str , Any ]:
965982 """Return available hydration data 'cdate' format 'YYYY-MM-DD'."""
@@ -1232,13 +1249,12 @@ def get_race_predictions(
12321249 return self .connectapi (url )
12331250
12341251 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 }
12421258 return self .connectapi (url , params = params )
12431259
12441260 else :
@@ -1247,6 +1263,7 @@ def get_race_predictions(
12471263 def get_training_status (self , cdate : str ) -> dict [str , Any ]:
12481264 """Return training status data for current user."""
12491265
1266+ cdate = _validate_date_format (cdate , "cdate" )
12501267 url = f"{ self .garmin_connect_training_status_url } /{ cdate } "
12511268 logger .debug ("Requesting training status data" )
12521269
@@ -1255,6 +1272,7 @@ def get_training_status(self, cdate: str) -> dict[str, Any]:
12551272 def get_fitnessage_data (self , cdate : str ) -> dict [str , Any ]:
12561273 """Return Fitness Age data for current user."""
12571274
1275+ cdate = _validate_date_format (cdate , "cdate" )
12581276 url = f"{ self .garmin_connect_fitnessage } /{ cdate } "
12591277 logger .debug ("Requesting Fitness Age data" )
12601278
@@ -1573,13 +1591,16 @@ def get_activities_by_date(
15731591 # 20 activities at a time
15741592 # and automatically loads more on scroll
15751593 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" )
15761597 params = {
1577- "startDate" : str ( startdate ) ,
1598+ "startDate" : startdate ,
15781599 "start" : str (start ),
15791600 "limit" : str (limit ),
15801601 }
15811602 if enddate :
1582- params ["endDate" ] = str ( enddate )
1603+ params ["endDate" ] = enddate
15831604 if activitytype :
15841605 params ["activityType" ] = str (activitytype )
15851606 if sortorder :
@@ -1865,6 +1886,7 @@ def request_reload(self, cdate: str) -> dict[str, Any]:
18651886 Garmin offloads older data.
18661887 """
18671888
1889+ cdate = _validate_date_format (cdate , "cdate" )
18681890 url = f"{ self .garmin_request_reload_url } /{ cdate } "
18691891 logger .debug (f"Requesting reload of data for { cdate } ." )
18701892
0 commit comments