mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-01-30 11:01:53 +00:00
migrate to garmin connect library
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from backend.app.services.garmin import GarminService, GarminAPIError, GarminAuthError
|
||||
from backend.app.services.garmin import GarminConnectService as GarminService, GarminAPIError, GarminAuthError
|
||||
from backend.app.models.workout import Workout
|
||||
from backend.app.models.garmin_sync_log import GarminSyncLog, GarminSyncStatus
|
||||
from datetime import datetime, timedelta
|
||||
@@ -20,46 +20,65 @@ class WorkoutSyncService:
|
||||
|
||||
async def sync_recent_activities(self, days_back: int = 7) -> int:
|
||||
"""Sync recent Garmin activities to database."""
|
||||
logger.info(f"Starting Garmin activity sync for the last {days_back} days.")
|
||||
sync_log = None # Initialize sync_log
|
||||
try:
|
||||
# Create sync log entry
|
||||
sync_log = GarminSyncLog(status="in_progress")
|
||||
sync_log = GarminSyncLog(status=GarminSyncStatus.IN_PROGRESS)
|
||||
self.db.add(sync_log)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(sync_log) # Refresh to get the generated ID
|
||||
|
||||
logger.debug(f"Created new GarminSyncLog with ID: {sync_log.id}")
|
||||
|
||||
# Calculate start date
|
||||
start_date = datetime.now() - timedelta(days=days_back)
|
||||
logger.debug(f"Fetching activities from Garmin starting from: {start_date}")
|
||||
|
||||
# Fetch activities from Garmin
|
||||
activities = await self.garmin_service.get_activities(
|
||||
limit=50, start_date=start_date
|
||||
limit=50, start_date=start_date, end_date=datetime.now()
|
||||
)
|
||||
logger.debug(f"Found {len(activities)} activities from Garmin.")
|
||||
|
||||
synced_count = 0
|
||||
for activity in activities:
|
||||
activity_id = activity['activityId']
|
||||
activity_id = str(activity['activityId'])
|
||||
logger.debug(f"Processing activity ID: {activity_id}")
|
||||
if await self.activity_exists(activity_id):
|
||||
logger.debug(f"Activity {activity_id} already exists in DB, skipping.")
|
||||
continue
|
||||
|
||||
# Get full activity details with retry logic
|
||||
max_retries = 3
|
||||
details = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.debug(f"Attempt {attempt + 1} to fetch details for activity {activity_id}")
|
||||
details = await self.garmin_service.get_activity_details(activity_id)
|
||||
logger.debug(f"Successfully fetched details for activity {activity_id}.")
|
||||
break
|
||||
except (GarminAPIError, GarminAuthError) as e:
|
||||
logger.warning(f"Failed to fetch details for {activity_id} (attempt {attempt + 1}/{max_retries}): {e}")
|
||||
if attempt == max_retries - 1:
|
||||
logger.error(f"Max retries reached for activity {activity_id}. Skipping details fetch.", exc_info=True)
|
||||
raise
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
logger.warning(f"Retrying activity details fetch for {activity_id}, attempt {attempt + 1}")
|
||||
|
||||
if details is None:
|
||||
logger.warning(f"Skipping activity {activity_id} due to failure in fetching details.")
|
||||
continue
|
||||
|
||||
# Merge basic activity data with detailed metrics
|
||||
full_activity = {**activity, **details}
|
||||
logger.debug(f"Merged activity data for {activity_id}.")
|
||||
|
||||
# Parse and create workout
|
||||
workout_data = await self.parse_activity_data(full_activity)
|
||||
workout = Workout(**workout_data)
|
||||
self.db.add(workout)
|
||||
synced_count += 1
|
||||
logger.debug(f"Added workout {workout.garmin_activity_id} to session.")
|
||||
|
||||
# Update sync log
|
||||
sync_log.status = GarminSyncStatus.COMPLETED
|
||||
@@ -67,48 +86,58 @@ class WorkoutSyncService:
|
||||
sync_log.last_sync_time = datetime.now()
|
||||
|
||||
await self.db.commit()
|
||||
logger.info(f"Successfully synced {synced_count} activities")
|
||||
logger.info(f"Successfully synced {synced_count} activities.")
|
||||
return synced_count
|
||||
|
||||
except GarminAuthError as e:
|
||||
sync_log.status = GarminSyncStatus.AUTH_FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
logger.error(f"Garmin authentication failed: {str(e)}")
|
||||
logger.error(f"Garmin authentication failed during sync: {e}", exc_info=True)
|
||||
if sync_log:
|
||||
sync_log.status = GarminSyncStatus.AUTH_FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
raise
|
||||
except GarminAPIError as e:
|
||||
sync_log.status = GarminSyncStatus.FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
logger.error(f"Garmin API error during sync: {str(e)}")
|
||||
logger.error(f"Garmin API error during sync: {e}", exc_info=True)
|
||||
if sync_log:
|
||||
sync_log.status = GarminSyncStatus.FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
raise
|
||||
except Exception as e:
|
||||
sync_log.status = GarminSyncStatus.FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
logger.error(f"Unexpected error during sync: {str(e)}")
|
||||
logger.error(f"Unexpected error during Garmin sync: {e}", exc_info=True)
|
||||
if sync_log:
|
||||
sync_log.status = GarminSyncStatus.FAILED
|
||||
sync_log.error_message = str(e)
|
||||
await self.db.commit()
|
||||
raise
|
||||
|
||||
async def get_latest_sync_status(self):
|
||||
"""Get the most recent sync log entry"""
|
||||
"""Get the most recent sync log entry."""
|
||||
logger.debug("Fetching latest Garmin sync status.")
|
||||
result = await self.db.execute(
|
||||
select(GarminSyncLog)
|
||||
.order_by(desc(GarminSyncLog.created_at))
|
||||
.limit(1)
|
||||
)
|
||||
return await result.scalar_one_or_none()
|
||||
status = result.scalar_one_or_none()
|
||||
logger.debug(f"Latest sync status: {status.status if status else 'None'}")
|
||||
return status
|
||||
|
||||
async def activity_exists(self, garmin_activity_id: str) -> bool:
|
||||
"""Check if activity already exists in database."""
|
||||
logger.debug(f"Checking if activity {garmin_activity_id} exists in database.")
|
||||
result = await self.db.execute(
|
||||
select(Workout).where(Workout.garmin_activity_id == garmin_activity_id)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None # Remove the await here
|
||||
exists = result.scalar_one_or_none() is not None
|
||||
logger.debug(f"Activity {garmin_activity_id} exists: {exists}")
|
||||
return exists
|
||||
|
||||
async def parse_activity_data(self, activity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Parse Garmin activity data into workout model format."""
|
||||
logger.debug(f"Parsing activity data for Garmin activity ID: {activity.get('activityId')}")
|
||||
return {
|
||||
"garmin_activity_id": activity['activityId'],
|
||||
"garmin_activity_id": str(activity['activityId']),
|
||||
"activity_type": activity.get('activityType', {}).get('typeKey'),
|
||||
"start_time": datetime.fromisoformat(activity['startTimeLocal'].replace('Z', '+00:00')),
|
||||
"duration_seconds": activity.get('duration'),
|
||||
|
||||
Reference in New Issue
Block a user