@@ -81,6 +81,11 @@ def _validate_positive_integer(value: int, param_name: str = "value") -> int:
8181 return value
8282
8383
84+ def _fmt_ts (dt : datetime ) -> str :
85+ # Use ms precision to match server expectations
86+ return dt .replace (tzinfo = None ).strftime ("%Y-%m-%dT%H:%M:%S.%f" )[:- 3 ]
87+
88+
8489class Garmin :
8590 """Class for fetching data from Garmin Connect."""
8691
@@ -615,8 +620,8 @@ def add_weigh_in(
615620 # Apply timezone offset to get UTC/GMT time
616621 dtGMT = dt .astimezone (timezone .utc )
617622 payload = {
618- "dateTimestamp" : dt . isoformat ()[: 19 ] + ".00 " ,
619- "gmtTimestamp" : dtGMT . isoformat ()[: 19 ] + ".00 " ,
623+ "dateTimestamp" : f" { _fmt_ts ( dt ) } .000 " ,
624+ "gmtTimestamp" : f" { _fmt_ts ( dtGMT ) } .000 " ,
620625 "unitKey" : unitKey ,
621626 "sourceType" : "MANUAL" ,
622627 "value" : weight ,
@@ -650,15 +655,15 @@ def add_weigh_in_with_timestamps(
650655 weight = _validate_positive_number (weight , "weight" )
651656 # Build the payload
652657 payload = {
653- "dateTimestamp" : dt . isoformat ()[: 19 ] + ".00 " , # Local time
654- "gmtTimestamp" : dtGMT . isoformat ()[: 19 ] + ".00 " , # GMT/UTC time
658+ "dateTimestamp" : f" { _fmt_ts ( dt ) } .000 " , # Local time
659+ "gmtTimestamp" : f" { _fmt_ts ( dtGMT ) } .000 " , # GMT/UTC time
655660 "unitKey" : unitKey ,
656661 "sourceType" : "MANUAL" ,
657662 "value" : weight ,
658663 }
659664
660665 # Debug log for payload
661- logger .debug (f "Adding weigh-in with explicit timestamps: { payload } " )
666+ logger .debug ("Adding weigh-in with explicit timestamps: %s" , payload )
662667
663668 # Make the POST request
664669 return self .garth .post ("connectapi" , url , json = payload ).json ()
@@ -686,6 +691,7 @@ def get_daily_weigh_ins(self, cdate: str) -> dict[str, Any]:
686691
687692 def delete_weigh_in (self , weight_pk : str , cdate : str ) -> Any :
688693 """Delete specific weigh-in."""
694+ cdate = _validate_date_format (cdate , "cdate" )
689695 url = f"{ self .garmin_connect_weight_url } /weight/{ cdate } /byversion/{ weight_pk } "
690696 logger .debug ("Deleting weigh-in" )
691697
@@ -769,8 +775,8 @@ def set_blood_pressure(
769775 # Apply timezone offset to get UTC/GMT time
770776 dtGMT = dt .astimezone (timezone .utc )
771777 payload = {
772- "measurementTimestampLocal" : dt . isoformat ()[: 19 ] + ".00 " ,
773- "measurementTimestampGMT" : dtGMT . isoformat ()[: 19 ] + ".00 " ,
778+ "measurementTimestampLocal" : f" { _fmt_ts ( dt ) } .000 " ,
779+ "measurementTimestampGMT" : f" { _fmt_ts ( dtGMT ) } .000 " ,
774780 "systolic" : systolic ,
775781 "diastolic" : diastolic ,
776782 "pulse" : pulse ,
@@ -837,7 +843,6 @@ def get_lactate_threshold(
837843 :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
838844 :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
839845 :param str (Optional) - aggregation: How to aggregate the data. Must be one of `daily`, `weekly`, `monthly`, `yearly`.
840-
841846 """
842847
843848 if latest :
@@ -898,6 +903,16 @@ def get_lactate_threshold(
898903 if end_date is None :
899904 end_date = date .today ().isoformat ()
900905
906+ # Normalize and validate
907+ if isinstance (start_date , date ):
908+ start_date = start_date .isoformat ()
909+ else :
910+ start_date = _validate_date_format (start_date , "start_date" )
911+ if isinstance (end_date , date ):
912+ end_date = end_date .isoformat ()
913+ else :
914+ end_date = _validate_date_format (end_date , "end_date" )
915+
901916 _valid_aggregations = {"daily" , "weekly" , "monthly" , "yearly" }
902917 if aggregation not in _valid_aggregations :
903918 raise ValueError (f"aggregation must be one of { _valid_aggregations } " )
@@ -944,14 +959,14 @@ def add_hydration_data(
944959 cdate = str (raw_date )
945960
946961 raw_ts = datetime .now ()
947- timestamp = datetime . strftime (raw_ts , "%Y-%m-%dT%H:%M:%S.%f" )
962+ timestamp = _fmt_ts (raw_ts )
948963
949964 elif cdate is not None and timestamp is None :
950965 # If cdate is not null, validate and use timestamp associated with midnight
951966 cdate = _validate_date_format (cdate , "cdate" )
952967 try :
953968 raw_ts = datetime .strptime (cdate , "%Y-%m-%d" )
954- timestamp = datetime . strftime (raw_ts , "%Y-%m-%dT%H:%M:%S.%f" )
969+ timestamp = _fmt_ts (raw_ts )
955970 except ValueError as e :
956971 raise ValueError (f"invalid cdate: { e } " ) from e
957972
@@ -964,7 +979,7 @@ def add_hydration_data(
964979 cdate = str (raw_ts .date ())
965980 except ValueError as e :
966981 raise ValueError (
967- f"Invalid timestamp format (expected YYYY-MM-DDTHH:MM:SS.ffffff ): { e } "
982+ f"Invalid timestamp format (expected YYYY-MM-DDTHH:MM:SS.mmm ): { e } "
968983 ) from e
969984 else :
970985 # Both provided - validate consistency
@@ -1425,7 +1440,7 @@ def get_activities(
14251440 if activitytype :
14261441 params ["activityType" ] = str (activitytype )
14271442
1428- logger .debug (f "Requesting activities from { start } with limit { limit } " )
1443+ logger .debug ("Requesting activities from %d with limit %d" , start , limit )
14291444
14301445 activities = self .connectapi (url , params = params )
14311446
@@ -1440,7 +1455,7 @@ def get_activities_fordate(self, fordate: str) -> dict[str, Any]:
14401455
14411456 fordate = _validate_date_format (fordate , "fordate" )
14421457 url = f"{ self .garmin_connect_activity_fordate } /{ fordate } "
1443- logger .debug (f "Requesting activities for date { fordate } " )
1458+ logger .debug ("Requesting activities for date %s" , fordate )
14441459
14451460 return self .connectapi (url )
14461461
@@ -1468,7 +1483,7 @@ def set_activity_type(
14681483 "parentTypeId" : parent_type_id ,
14691484 },
14701485 }
1471- logger .debug (f "Changing activity type: { str ( payload ) } " )
1486+ logger .debug ("Changing activity type: %s" , payload )
14721487 return self .garth .put ("connectapi" , url , json = payload , api = True )
14731488
14741489 def create_manual_activity_from_json (self , payload : dict [str , Any ]) -> Any :
@@ -1634,10 +1649,10 @@ def get_activities_by_date(
16341649 if sortorder :
16351650 params ["sortOrder" ] = str (sortorder )
16361651
1637- logger .debug (f "Requesting activities by date from { startdate } to { enddate } " )
1652+ logger .debug ("Requesting activities by date from %s to %s" , startdate , enddate )
16381653 while True :
16391654 params ["start" ] = str (start )
1640- logger .debug (f "Requesting activities { start } to { start + limit } " )
1655+ logger .debug ("Requesting activities %d to %d" , start , start + limit )
16411656 act = self .connectapi (url , params = params )
16421657 if act :
16431658 activities .extend (act )
@@ -1675,7 +1690,9 @@ def get_progress_summary_between_dates(
16751690 "metric" : str (metric ),
16761691 }
16771692
1678- logger .debug (f"Requesting fitnessstats by date from { startdate } to { enddate } " )
1693+ logger .debug (
1694+ "Requesting fitnessstats by date from %s to %s" , startdate , enddate
1695+ )
16791696 return self .connectapi (url , params = params )
16801697
16811698 def get_activity_types (self ) -> dict [str , Any ]:
@@ -1708,10 +1725,12 @@ def get_goals(
17081725 "sortOrder" : "asc" ,
17091726 }
17101727
1711- logger .debug (f "Requesting { status } goals" )
1728+ logger .debug ("Requesting %s goals" , status )
17121729 while True :
17131730 params ["start" ] = str (start )
1714- logger .debug (f"Requesting { status } goals { start } to { start + limit - 1 } " )
1731+ logger .debug (
1732+ "Requesting %s goals %d to %d" , status , start , start + limit - 1
1733+ )
17151734 goals_json = self .connectapi (url , params = params )
17161735 if goals_json :
17171736 goals .extend (goals_json )
@@ -1891,7 +1910,7 @@ def get_gear_activities(
18911910 :return: List of activities where the specified gear was used
18921911 """
18931912 gearUUID = str (gearUUID )
1894-
1913+ limit = _validate_positive_integer ( limit , "limit" )
18951914 url = f"{ self .garmin_connect_activities_baseurl } { gearUUID } /gear?start=0&limit={ limit } "
18961915 logger .debug ("Requesting activities for gearUUID %s" , gearUUID )
18971916
@@ -1921,7 +1940,7 @@ def request_reload(self, cdate: str) -> dict[str, Any]:
19211940
19221941 cdate = _validate_date_format (cdate , "cdate" )
19231942 url = f"{ self .garmin_request_reload_url } /{ cdate } "
1924- logger .debug (f "Requesting reload of data for { cdate } ." )
1943+ logger .debug ("Requesting reload of data for %s." , cdate )
19251944
19261945 return self .garth .post ("connectapi" , url , api = True ).json ()
19271946
@@ -1931,7 +1950,7 @@ def get_workouts(self, start: int = 0, limit: int = 100) -> dict[str, Any]:
19311950 url = f"{ self .garmin_workouts } /workouts"
19321951 start = _validate_non_negative_integer (start , "start" )
19331952 limit = _validate_positive_integer (limit , "limit" )
1934- logger .debug (f "Requesting workouts from { start } with limit { limit } " )
1953+ logger .debug ("Requesting workouts from %d with limit %d" , start , limit )
19351954 params = {"start" : start , "limit" : limit }
19361955 return self .connectapi (url , params = params )
19371956
@@ -1977,7 +1996,7 @@ def get_menstrual_data_for_date(self, fordate: str) -> dict[str, Any]:
19771996
19781997 fordate = _validate_date_format (fordate , "fordate" )
19791998 url = f"{ self .garmin_connect_menstrual_dayview_url } /{ fordate } "
1980- logger .debug (f "Requesting menstrual data for date { fordate } " )
1999+ logger .debug ("Requesting menstrual data for date %s" , fordate )
19812000
19822001 return self .connectapi (url )
19832002
@@ -1990,7 +2009,7 @@ def get_menstrual_calendar_data(
19902009 enddate = _validate_date_format (enddate , "enddate" )
19912010 url = f"{ self .garmin_connect_menstrual_calendar_url } /{ startdate } /{ enddate } "
19922011 logger .debug (
1993- f "Requesting menstrual data for dates { startdate } through { enddate } "
2012+ "Requesting menstrual data for dates %s through %s" , startdate , enddate
19942013 )
19952014
19962015 return self .connectapi (url )
0 commit comments