From 38b4529ecf59b16b96e3c15dd522a8843ff84857 Mon Sep 17 00:00:00 2001 From: sstent Date: Sat, 4 Oct 2025 16:17:25 -0700 Subject: [PATCH] sync --- README.md | 36 ++++- analyzers/workout_analyzer.py | 4 +- clients/garmin_client.py | 4 +- main.py | 38 +++-- parsers/file_parser.py | 4 +- requirements.txt | 8 +- visualizers/chart_generator.py | 2 +- visualizers/report_generator.py | 2 +- visualizers/templates/workout_report.html | 167 ++++++++++++++++++++++ visualizers/templates/workout_report.md | 38 +++++ 10 files changed, 275 insertions(+), 28 deletions(-) create mode 100644 visualizers/templates/workout_report.html create mode 100644 visualizers/templates/workout_report.md 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 @@ + + + + + + Workout Report - {{ workout.metadata.activity_name }} + + + +
+

Workout Report: {{ workout.metadata.activity_name }}

+

Date: {{ workout.metadata.start_time }}

+

Activity Type: {{ workout.metadata.activity_type }}

+ +

Summary

+
+
+

Duration

+
{{ workout.summary.duration_minutes|format_duration }}
+
+
+

Distance

+
{{ workout.summary.distance_km|format_distance }}
+
+
+

Avg Power

+
{{ workout.summary.avg_power|format_power }}
+
+
+

Avg Heart Rate

+
{{ workout.summary.avg_heart_rate|format_heart_rate }}
+
+
+

Avg Speed

+
{{ workout.summary.avg_speed_kmh|format_speed }}
+
+
+

Calories

+
{{ workout.summary.calories|int }}
+
+
+ +

Detailed Analysis

+ +

Power Analysis

+ + + + + + + + + + + + + + + + + + + + + +
MetricValue
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) }}
+ +

Heart Rate Analysis

+ + + + + + + + + + + + + +
MetricValue
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 }}
+ +

Speed Analysis

+ + + + + + + + + + + + + +
MetricValue
Average Speed{{ workout.speed_analysis.avg_speed|format_speed }}
Maximum Speed{{ workout.speed_analysis.max_speed|format_speed }}
+ + +
+ + \ No newline at end of file diff --git a/visualizers/templates/workout_report.md b/visualizers/templates/workout_report.md new file mode 100644 index 0000000..c3139f4 --- /dev/null +++ b/visualizers/templates/workout_report.md @@ -0,0 +1,38 @@ +# Workout Report: {{ workout.metadata.activity_name }} + +**Date:** {{ workout.metadata.start_time }} +**Activity Type:** {{ workout.metadata.activity_type }} + +## Summary + +| Metric | Value | +|--------|--------| +| Duration | {{ workout.summary.duration_minutes|format_duration }} | +| Distance | {{ workout.summary.distance_km|format_distance }} | +| Average Power | {{ workout.summary.avg_power|format_power }} | +| Average Heart Rate | {{ workout.summary.avg_heart_rate|format_heart_rate }} | +| Average Speed | {{ workout.summary.avg_speed_kmh|format_speed }} | +| Calories | {{ workout.summary.calories|int }} | + +## Detailed Analysis + +### Power Analysis + +- **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) }} + +### Heart Rate Analysis + +- **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 }} + +### Speed Analysis + +- **Average Speed:** {{ workout.speed_analysis.avg_speed|format_speed }} +- **Maximum Speed:** {{ workout.speed_analysis.max_speed|format_speed }} + +--- + +*Report generated on {{ report.generated_at }} using {{ report.tool }} v{{ report.version }}* \ No newline at end of file