diff --git a/README.md b/README.md index 734f28a..6860d64 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,11 @@ python main.py --garmin-connect --report --charts --summary ### Command Line Options ``` -usage: main.py [-h] [--config CONFIG] [--verbose] (--file FILE | --directory DIRECTORY | --garmin-connect) - [--ftp FTP] [--max-hr MAX_HR] [--zones ZONES] [--output-dir OUTPUT_DIR] - [--format {html,pdf,markdown}] [--charts] [--report] [--summary] +usage: main.py [-h] [--config CONFIG] [--verbose] + (--file FILE | --directory DIRECTORY | --garmin-connect | --workout-id WORKOUT_ID | --download-all | --reanalyze-all) + [--ftp FTP] [--max-hr MAX_HR] [--zones ZONES] [--cog COG] + [--output-dir OUTPUT_DIR] [--format {html,pdf,markdown}] + [--charts] [--report] [--summary] Analyze Garmin workout data from files or Garmin Connect @@ -66,13 +68,24 @@ options: --config CONFIG, -c CONFIG Configuration file path --verbose, -v Enable verbose logging + +Input options: --file FILE, -f FILE Path to workout file (FIT, TCX, or GPX) --directory DIRECTORY, -d DIRECTORY Directory containing workout files --garmin-connect Download from Garmin Connect + --workout-id WORKOUT_ID + Analyze specific workout by ID from Garmin Connect + --download-all Download all cycling activities from Garmin Connect (no analysis) + --reanalyze-all Re-analyze all downloaded activities and generate reports + +Analysis options: --ftp FTP Functional Threshold Power (W) --max-hr MAX_HR Maximum heart rate (bpm) --zones ZONES Path to zones configuration file + --cog COG Cog size (teeth) for power calculations. Auto-detected if not provided + +Output options: --output-dir OUTPUT_DIR Output directory for reports and charts --format {html,pdf,markdown} @@ -80,6 +93,23 @@ options: --charts Generate charts --report Generate comprehensive report --summary Generate summary report for multiple workouts + +Examples: + Analyze latest workout from Garmin Connect: python main.py --garmin-connect + Analyze specific workout by ID: python main.py --workout-id 123456789 + Download all cycling workouts: python main.py --download-all + Re-analyze all downloaded workouts: python main.py --reanalyze-all + Analyze local FIT file: python main.py --file path/to/workout.fit + Analyze directory of workouts: python main.py --directory data/ + +Configuration: + Set Garmin credentials in .env file: GARMIN_EMAIL and GARMIN_PASSWORD + Configure zones in config/config.yaml or use --zones flag + Override FTP with --ftp flag, max HR with --max-hr flag + +Output: + Reports saved to output/ directory by default + Charts saved to output/charts/ when --charts is used ``` ## Configuration diff --git a/analyzers/workout_analyzer.py b/analyzers/workout_analyzer.py index a9f720f..864547e 100644 --- a/analyzers/workout_analyzer.py +++ b/analyzers/workout_analyzer.py @@ -6,8 +6,8 @@ import pandas as pd from typing import Dict, List, Optional, Tuple, Any from datetime import timedelta -from ..models.workout import WorkoutData, PowerData, HeartRateData, SpeedData, ElevationData -from ..models.zones import ZoneCalculator +from models.workout import WorkoutData, PowerData, HeartRateData, SpeedData, ElevationData +from models.zones import ZoneCalculator logger = logging.getLogger(__name__) diff --git a/clients/garmin_client.py b/clients/garmin_client.py index ac80998..673a769 100644 --- a/clients/garmin_client.py +++ b/clients/garmin_client.py @@ -12,7 +12,7 @@ try: except ImportError: raise ImportError("garminconnect package required. Install with: pip install garminconnect") -from ..config.settings import GARMIN_EMAIL, GARMIN_PASSWORD, DATA_DIR +from config.settings import GARMIN_EMAIL, GARMIN_PASSWORD, DATA_DIR logger = logging.getLogger(__name__) @@ -176,7 +176,7 @@ class GarminClient: DATA_DIR.mkdir(exist_ok=True) # Download original file - file_data = self.client.download_original_activity(activity_id) + file_data = self.client.download_activity(activity_id, dl_fmt='ZIP') # Save to temporary file first with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_file: diff --git a/main.py b/main.py index 153418e..cf6df23 100644 --- a/main.py +++ b/main.py @@ -241,23 +241,37 @@ class GarminAnalyser: logging.info(f"Downloading workouts from Garmin Connect (last {days} days)") client = GarminClient( - username=settings.GARMIN_EMAIL, + email=settings.GARMIN_EMAIL, password=settings.GARMIN_PASSWORD ) # Download workouts - workouts = client.get_workouts(days=days) + workouts = client.get_all_cycling_workouts() # Analyze each workout results = [] - for workout in workouts: + for workout_summary in workouts: try: - analysis = self.workout_analyzer.analyze_workout(workout) - results.append({ - 'workout': workout, - 'analysis': analysis, - 'file_path': None - }) + activity_id = workout_summary.get('activityId') + if not activity_id: + logging.warning("Skipping workout with no activity ID.") + continue + + logging.info(f"Downloading workout file for activity ID: {activity_id}") + workout_file_path = client.download_activity_original(str(activity_id)) + + if workout_file_path and workout_file_path.exists(): + workout = self.file_parser.parse_file(workout_file_path) + if workout: + analysis = self.workout_analyzer.analyze_workout(workout) + results.append({ + 'workout': workout, + 'analysis': analysis, + 'file_path': workout_file_path + }) + else: + logging.error(f"Failed to download workout file for activity ID: {activity_id}") + except Exception as e: logging.error(f"Error analyzing workout: {e}") @@ -269,10 +283,8 @@ class GarminAnalyser: Returns: List of downloaded workouts """ - logging.info("Downloading all cycling activities from Garmin Connect") - client = GarminClient( - username=settings.GARMIN_EMAIL, + email=settings.GARMIN_EMAIL, password=settings.GARMIN_PASSWORD ) @@ -346,7 +358,7 @@ class GarminAnalyser: logging.info(f"Analyzing workout ID: {workout_id}") client = GarminClient( - username=settings.GARMIN_EMAIL, + email=settings.GARMIN_EMAIL, password=settings.GARMIN_PASSWORD ) diff --git a/parsers/file_parser.py b/parsers/file_parser.py index 3783698..2a38fee 100644 --- a/parsers/file_parser.py +++ b/parsers/file_parser.py @@ -11,8 +11,8 @@ try: except ImportError: raise ImportError("fitparse package required. Install with: pip install fitparse") -from ..models.workout import WorkoutData, WorkoutMetadata, PowerData, HeartRateData, SpeedData, ElevationData, GearData -from ..config.settings import SUPPORTED_FORMATS +from models.workout import WorkoutData, WorkoutMetadata, PowerData, HeartRateData, SpeedData, ElevationData, GearData +from config.settings import SUPPORTED_FORMATS logger = logging.getLogger(__name__) diff --git a/requirements.txt b/requirements.txt index d21b14d..85aff7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ fitparse==1.2.0 -garminconnect==0.2.28 +garminconnect==0.2.30 Jinja2==3.1.6 -Markdown==3.8.2 +Markdown==3.9 matplotlib==3.10.6 -numpy==2.3.2 +numpy==2.3.3 pandas==2.3.2 plotly==6.3.0 python-dotenv==1.1.1 python_magic==0.4.27 seaborn==0.13.2 -setuptools==75.8.0 +setuptools==80.9.0 weasyprint==66.0 diff --git a/visualizers/chart_generator.py b/visualizers/chart_generator.py index 5b5e0f8..df17033 100644 --- a/visualizers/chart_generator.py +++ b/visualizers/chart_generator.py @@ -11,7 +11,7 @@ import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots -from ..models.workout import WorkoutData +from models.workout import WorkoutData logger = logging.getLogger(__name__) diff --git a/visualizers/report_generator.py b/visualizers/report_generator.py index ee50b64..f1fc997 100644 --- a/visualizers/report_generator.py +++ b/visualizers/report_generator.py @@ -10,7 +10,7 @@ from markdown import markdown from weasyprint import HTML, CSS import json -from ..models.workout import WorkoutData +from models.workout import WorkoutData logger = logging.getLogger(__name__) diff --git a/visualizers/templates/workout_report.html b/visualizers/templates/workout_report.html new file mode 100644 index 0000000..781e07d --- /dev/null +++ b/visualizers/templates/workout_report.html @@ -0,0 +1,167 @@ + + +
+ + +Date: {{ workout.metadata.start_time }}
+Activity Type: {{ workout.metadata.activity_type }}
+ +| Metric | +Value | +
|---|---|
| Average Power | +{{ workout.power_analysis.avg_power|format_power }} | +
| Maximum Power | +{{ workout.power_analysis.max_power|format_power }} | +
| Normalized Power | +{{ workout.summary.normalized_power|format_power }} | +
| Intensity Factor | +{{ "%.2f"|format(workout.summary.intensity_factor) }} | +
| Metric | +Value | +
|---|---|
| Average Heart Rate | +{{ workout.heart_rate_analysis.avg_heart_rate|format_heart_rate }} | +
| Maximum Heart Rate | +{{ workout.heart_rate_analysis.max_heart_rate|format_heart_rate }} | +
| Metric | +Value | +
|---|---|
| Average Speed | +{{ workout.speed_analysis.avg_speed|format_speed }} | +
| Maximum Speed | +{{ workout.speed_analysis.max_speed|format_speed }} | +