diff --git a/fitbitsync.py b/fitbitsync.py index 100fb97..56bd872 100644 --- a/fitbitsync.py +++ b/fitbitsync.py @@ -22,10 +22,16 @@ except ImportError: FITBIT_LIBRARY = False try: - import garminconnect - GARMIN_LIBRARY = "garminconnect" + import garth + GARTH_LIBRARY = True except ImportError: - GARMIN_LIBRARY = None + GARTH_LIBRARY = False + +try: + import garminconnect + GARMINCONNECT_LIBRARY = True +except ImportError: + GARMINCONNECT_LIBRARY = False import schedule @@ -555,34 +561,12 @@ class GarminClient: self.garminconnect = garminconnect logger.info("Using garminconnect library") - # Monkey patch the login method to handle garth compatibility issue - original_login = self.garminconnect.Garmin.login - - def patched_login(self): - """Patched login method that handles garth returning None""" - try: - result = original_login(self) - return result - except TypeError as e: - if "cannot unpack non-iterable NoneType object" in str(e): - # Check if we have valid tokens despite the None return - if (self.garth.oauth1_token and self.garth.oauth2_token): - logger.info("Login successful (handled garth None return)") - return True - else: - raise - else: - raise - - # Apply the patch - self.garminconnect.Garmin.login = patched_login - except ImportError: logger.error("garminconnect library not installed. Install with: pip install garminconnect") raise ImportError("garminconnect library is required but not installed") async def authenticate(self) -> bool: - """Authenticate with Garmin Connect""" + """Authenticate with Garmin Connect using garth""" if self.read_only_mode: logger.info("Running in read-only mode - skipping Garmin authentication") return True @@ -597,54 +581,24 @@ class GarminClient: if not self._setup_credentials(): return False - logger.info("Initializing Garmin client...") + # Set session file path for garminconnect library + os.environ['GARMINTOKENS'] = str(self.session_file) + + # Configure garth for domain if using Garmin China + if self.is_china: + garth.configure(domain="garmin.cn") + + # Initialize garminconnect.Garmin with credentials. + # It will use garth library for authentication and session management. self.garmin_client = self.garminconnect.Garmin( - self.username, - self.password, - is_cn=self.is_china + self.username, self.password ) + self.garmin_client.login() - # Use garth to load the session if it exists - if os.path.exists(self.session_file): - try: - logger.info(f"Attempting to load session from {self.session_file}") - self.garmin_client.garth.load(self.session_file) - logger.info("Loaded existing session from file.") - # Log garth state after loading - logger.info(f"Garth tokens after load: oauth1={bool(self.garmin_client.garth.oauth1_token)}, oauth2={bool(self.garmin_client.garth.oauth2_token)}") - except Exception as e: - logger.warning(f"Could not load session file: {e}. Performing fresh login.") - - # Login (will use loaded session or perform a fresh auth) - logger.info("Calling garmin_client.login()...") - try: - # Handle garth API compatibility issue - newer versions return None - # when using existing sessions, but garminconnect expects a tuple - login_result = self.garmin_client.login() - - # Check if login returned None (new garth behavior with existing sessions) - if login_result is None: - # Verify that we actually have valid tokens after login - if (self.garmin_client.garth.oauth1_token and - self.garmin_client.garth.oauth2_token): - logger.info("Login successful (garth returned None but tokens are valid)") - else: - logger.error("Login failed - garth returned None and no valid tokens") - raise Exception("Garmin login failed: No valid tokens after authentication") - else: - logger.info("Login successful") - - except Exception as e: - logger.error(f"Login failed with exception: {e}") - # Log garth state before re-raising - logger.info(f"Garth tokens before failure: oauth1={bool(self.garmin_client.garth.oauth1_token)}, oauth2={bool(self.garmin_client.garth.oauth2_token)}") - raise - - # Save the session using garth's dump method - self.garmin_client.garth.dump(self.session_file) - + # Verify by getting the full name profile = self.garmin_client.get_full_name() - logger.info(f"Successfully authenticated and saved session for user: {profile}") + logger.info(f"Successfully authenticated with Garmin for user: {profile}") + return True except Exception as e: @@ -653,6 +607,10 @@ class GarminClient: logger.error(f"Full traceback: {traceback.format_exc()}") return False + def _mfa_handler(self, _) -> str: + """Handle MFA code input from the user.""" + return input("Enter Garmin MFA code: ") + def _setup_credentials(self) -> bool: """Setup Garmin credentials interactively""" print("\nšŸ”‘ Garmin Connect Credentials Setup")