This commit is contained in:
2025-12-14 06:08:30 -08:00
parent 3a4563a34d
commit 399132006c

View File

@@ -22,10 +22,16 @@ except ImportError:
FITBIT_LIBRARY = False FITBIT_LIBRARY = False
try: try:
import garminconnect import garth
GARMIN_LIBRARY = "garminconnect" GARTH_LIBRARY = True
except ImportError: except ImportError:
GARMIN_LIBRARY = None GARTH_LIBRARY = False
try:
import garminconnect
GARMINCONNECT_LIBRARY = True
except ImportError:
GARMINCONNECT_LIBRARY = False
import schedule import schedule
@@ -555,34 +561,12 @@ class GarminClient:
self.garminconnect = garminconnect self.garminconnect = garminconnect
logger.info("Using garminconnect library") 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: except ImportError:
logger.error("garminconnect library not installed. Install with: pip install garminconnect") logger.error("garminconnect library not installed. Install with: pip install garminconnect")
raise ImportError("garminconnect library is required but not installed") raise ImportError("garminconnect library is required but not installed")
async def authenticate(self) -> bool: async def authenticate(self) -> bool:
"""Authenticate with Garmin Connect""" """Authenticate with Garmin Connect using garth"""
if self.read_only_mode: if self.read_only_mode:
logger.info("Running in read-only mode - skipping Garmin authentication") logger.info("Running in read-only mode - skipping Garmin authentication")
return True return True
@@ -597,54 +581,24 @@ class GarminClient:
if not self._setup_credentials(): if not self._setup_credentials():
return False 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.garmin_client = self.garminconnect.Garmin(
self.username, self.username, self.password
self.password,
is_cn=self.is_china
) )
self.garmin_client.login()
# Use garth to load the session if it exists # Verify by getting the full name
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)
profile = self.garmin_client.get_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 return True
except Exception as e: except Exception as e:
@@ -653,6 +607,10 @@ class GarminClient:
logger.error(f"Full traceback: {traceback.format_exc()}") logger.error(f"Full traceback: {traceback.format_exc()}")
return False 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: def _setup_credentials(self) -> bool:
"""Setup Garmin credentials interactively""" """Setup Garmin credentials interactively"""
print("\n🔑 Garmin Connect Credentials Setup") print("\n🔑 Garmin Connect Credentials Setup")