mirror of
https://github.com/sstent/Garmin_Analyser.git
synced 2026-01-25 16:42:40 +00:00
sync
This commit is contained in:
@@ -45,7 +45,7 @@ except ImportError as e:
|
|||||||
class GarminWorkoutAnalyzer:
|
class GarminWorkoutAnalyzer:
|
||||||
"""Main class for analyzing Garmin workout data."""
|
"""Main class for analyzing Garmin workout data."""
|
||||||
|
|
||||||
def __init__(self, is_indoor=False):
|
def __init__(self):
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -63,13 +63,16 @@ class GarminWorkoutAnalyzer:
|
|||||||
38: [14, 16, 18, 20],
|
38: [14, 16, 18, 20],
|
||||||
46: [16]
|
46: [16]
|
||||||
}
|
}
|
||||||
self.is_indoor = is_indoor
|
self.is_indoor = False
|
||||||
self.selected_chainring = None
|
self.selected_chainring = None
|
||||||
self.power_data_available = False
|
self.power_data_available = False
|
||||||
self.CHAINRING_TEETH = 38 # Default, will be updated
|
self.CHAINRING_TEETH = 38 # Default, will be updated
|
||||||
self.BIKE_WEIGHT_LBS = 22
|
self.BIKE_WEIGHT_LBS = 22
|
||||||
self.BIKE_WEIGHT_KG = self.BIKE_WEIGHT_LBS * 0.453592
|
self.BIKE_WEIGHT_KG = self.BIKE_WEIGHT_LBS * 0.453592
|
||||||
|
|
||||||
|
# Indoor activity keywords
|
||||||
|
self.INDOOR_KEYWORDS = ['indoor_cycling', 'indoor cycling', 'indoor bike', 'trainer', 'zwift', 'virtual']
|
||||||
|
|
||||||
# HR Zones (based on LTHR 170 bpm)
|
# HR Zones (based on LTHR 170 bpm)
|
||||||
self.HR_ZONES = {
|
self.HR_ZONES = {
|
||||||
'Z1': (0, 136),
|
'Z1': (0, 136),
|
||||||
@@ -103,6 +106,20 @@ class GarminWorkoutAnalyzer:
|
|||||||
self.TIRE_CIRCUMFERENCE_MM = math.pi * (self.WHEEL_DIAMETER_MM + 2 * self.TIRE_WIDTH_MM)
|
self.TIRE_CIRCUMFERENCE_MM = math.pi * (self.WHEEL_DIAMETER_MM + 2 * self.TIRE_WIDTH_MM)
|
||||||
self.TIRE_CIRCUMFERENCE_M = self.TIRE_CIRCUMFERENCE_MM / 1000 # ~2.23m
|
self.TIRE_CIRCUMFERENCE_M = self.TIRE_CIRCUMFERENCE_MM / 1000 # ~2.23m
|
||||||
|
|
||||||
|
def _detect_indoor_activity(self, activity):
|
||||||
|
"""Detect if activity is indoor based on type and name."""
|
||||||
|
activity_type = activity.get('activityType', {}).get('typeKey', '').lower()
|
||||||
|
activity_name = activity.get('activityName', '').lower()
|
||||||
|
|
||||||
|
self.is_indoor = any(
|
||||||
|
keyword in activity_type or keyword in activity_name
|
||||||
|
for keyword in self.INDOOR_KEYWORDS
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.is_indoor:
|
||||||
|
print(f"Detected indoor activity: {activity_name} (Type: {activity_type})")
|
||||||
|
else:
|
||||||
|
print(f"Detected outdoor activity: {activity_name} (Type: {activity_type})")
|
||||||
|
|
||||||
def connect_to_garmin(self) -> bool:
|
def connect_to_garmin(self) -> bool:
|
||||||
"""Connect to Garmin Connect using credentials from .env file."""
|
"""Connect to Garmin Connect using credentials from .env file."""
|
||||||
@@ -127,6 +144,11 @@ class GarminWorkoutAnalyzer:
|
|||||||
try:
|
try:
|
||||||
print(f"Downloading workout ID: {activity_id}")
|
print(f"Downloading workout ID: {activity_id}")
|
||||||
self.last_activity_id = activity_id
|
self.last_activity_id = activity_id
|
||||||
|
|
||||||
|
# Get activity details to detect indoor type
|
||||||
|
activity = self.garmin_client.get_activity(activity_id)
|
||||||
|
self._detect_indoor_activity(activity)
|
||||||
|
|
||||||
return self._download_workout(activity_id)
|
return self._download_workout(activity_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error downloading workout {activity_id}: {e}")
|
print(f"Error downloading workout {activity_id}: {e}")
|
||||||
@@ -184,6 +206,10 @@ class GarminWorkoutAnalyzer:
|
|||||||
activity_id = cycling_activity['activityId']
|
activity_id = cycling_activity['activityId']
|
||||||
self.last_activity_id = activity_id
|
self.last_activity_id = activity_id
|
||||||
print(f"Found cycling activity: {cycling_activity['activityName']} ({activity_id})")
|
print(f"Found cycling activity: {cycling_activity['activityName']} ({activity_id})")
|
||||||
|
|
||||||
|
# Detect indoor activity type
|
||||||
|
self._detect_indoor_activity(cycling_activity)
|
||||||
|
|
||||||
return self._download_workout(activity_id)
|
return self._download_workout(activity_id)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -343,6 +369,9 @@ class GarminWorkoutAnalyzer:
|
|||||||
print(f" Already exists: {existing_files[0]}")
|
print(f" Already exists: {existing_files[0]}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Detect indoor activity type for each activity
|
||||||
|
self._detect_indoor_activity(activity)
|
||||||
|
|
||||||
self._download_workout(activity_id)
|
self._download_workout(activity_id)
|
||||||
|
|
||||||
print("\nAll cycling activities downloaded")
|
print("\nAll cycling activities downloaded")
|
||||||
@@ -755,10 +784,10 @@ class GarminWorkoutAnalyzer:
|
|||||||
if 'timestamp' in df.columns:
|
if 'timestamp' in df.columns:
|
||||||
df = df.dropna(subset=['timestamp'])
|
df = df.dropna(subset=['timestamp'])
|
||||||
|
|
||||||
# Fill other missing values with defaults
|
# Fill other missing values with defaults using proper assignment
|
||||||
for col in ['heart_rate', 'cadence', 'speed', 'distance', 'altitude', 'temperature']:
|
for col in ['heart_rate', 'cadence', 'speed', 'distance', 'altitude', 'temperature']:
|
||||||
if col in df.columns:
|
if col in df.columns:
|
||||||
df[col].fillna(0, inplace=True)
|
df[col] = df[col].fillna(0)
|
||||||
|
|
||||||
return self._process_workout_data(df, session_data, cog_size)
|
return self._process_workout_data(df, session_data, cog_size)
|
||||||
|
|
||||||
@@ -1104,9 +1133,9 @@ class GarminWorkoutAnalyzer:
|
|||||||
# For indoor workouts, gradient calculation is simulated
|
# For indoor workouts, gradient calculation is simulated
|
||||||
df['gradient'] = 0
|
df['gradient'] = 0
|
||||||
|
|
||||||
# Fixed gear configuration for indoor bike
|
# Fixed gear configuration for indoor bike (38t chainring + 14t cog)
|
||||||
self.selected_chainring = 38
|
self.selected_chainring = 38
|
||||||
cog_size = 16
|
cog_size = 14 # Set explicitly for indoor
|
||||||
self.CHAINRING_TEETH = self.selected_chainring
|
self.CHAINRING_TEETH = self.selected_chainring
|
||||||
|
|
||||||
# Use physics model for indoor power estimation
|
# Use physics model for indoor power estimation
|
||||||
@@ -1658,6 +1687,7 @@ class GarminWorkoutAnalyzer:
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to run the workout analyzer."""
|
"""Main function to run the workout analyzer."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@@ -1674,12 +1704,12 @@ def main():
|
|||||||
formatter_class=argparse.RawTextHelpFormatter
|
formatter_class=argparse.RawTextHelpFormatter
|
||||||
)
|
)
|
||||||
parser.add_argument('-w', '--workout-id', type=int, help='Analyze specific workout by ID')
|
parser.add_argument('-w', '--workout-id', type=int, help='Analyze specific workout by ID')
|
||||||
parser.add_argument('--indoor', action='store_true', help='Process as indoor cycling workout')
|
|
||||||
parser.add_argument('--download-all', action='store_true', help='Download all cycling activities (no analysis)')
|
parser.add_argument('--download-all', action='store_true', help='Download all cycling activities (no analysis)')
|
||||||
parser.add_argument('--reanalyze-all', action='store_true', help='Re-analyze all downloaded activities')
|
parser.add_argument('--reanalyze-all', action='store_true', help='Re-analyze all downloaded activities')
|
||||||
|
# Removed deprecated --indoor flag
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
analyzer = GarminWorkoutAnalyzer(is_indoor=args.indoor)
|
analyzer = GarminWorkoutAnalyzer()
|
||||||
|
|
||||||
# Step 1: Connect to Garmin
|
# Step 1: Connect to Garmin
|
||||||
if not analyzer.connect_to_garmin():
|
if not analyzer.connect_to_garmin():
|
||||||
@@ -1702,8 +1732,9 @@ def main():
|
|||||||
print(f"Failed to download workout {activity_id}")
|
print(f"Failed to download workout {activity_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Auto-detect indoor/outdoor and get cog size
|
||||||
estimated_cog = analyzer.estimate_cog_from_cadence(fit_file_path)
|
estimated_cog = analyzer.estimate_cog_from_cadence(fit_file_path)
|
||||||
confirmed_cog = analyzer.get_user_cog_confirmation(estimated_cog)
|
confirmed_cog = 14 if analyzer.is_indoor else analyzer.get_user_cog_confirmation(estimated_cog)
|
||||||
print("Analyzing workout with enhanced power calculations...")
|
print("Analyzing workout with enhanced power calculations...")
|
||||||
analysis_data = analyzer.analyze_fit_file(fit_file_path, confirmed_cog)
|
analysis_data = analyzer.analyze_fit_file(fit_file_path, confirmed_cog)
|
||||||
|
|
||||||
@@ -1725,8 +1756,9 @@ def main():
|
|||||||
print("Failed to download latest workout")
|
print("Failed to download latest workout")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Auto-detect indoor/outdoor and get cog size
|
||||||
estimated_cog = analyzer.estimate_cog_from_cadence(fit_file_path)
|
estimated_cog = analyzer.estimate_cog_from_cadence(fit_file_path)
|
||||||
confirmed_cog = analyzer.get_user_cog_confirmation(estimated_cog)
|
confirmed_cog = 14 if analyzer.is_indoor else analyzer.get_user_cog_confirmation(estimated_cog)
|
||||||
print("Analyzing with enhanced power model...")
|
print("Analyzing with enhanced power model...")
|
||||||
analysis_data = analyzer.analyze_fit_file(fit_file_path, confirmed_cog)
|
analysis_data = analyzer.analyze_fit_file(fit_file_path, confirmed_cog)
|
||||||
|
|
||||||
|
|||||||
31
todo.md
Normal file
31
todo.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 46-Tooth Chainring Detection Implementation
|
||||||
|
|
||||||
|
## Analysis Phase
|
||||||
|
- [x] Review current code structure
|
||||||
|
- [x] Identify chainring usage locations
|
||||||
|
- [x] Plan implementation approach
|
||||||
|
|
||||||
|
## Core Implementation
|
||||||
|
- [ ] Modify bike specifications to support multiple chainrings
|
||||||
|
- [ ] Update gear estimation algorithm for chainring detection
|
||||||
|
- [ ] Enhance cog estimation to determine chainring usage
|
||||||
|
- [ ] Update power calculations to use detected chainring
|
||||||
|
- [ ] Modify reporting to show detected chainring
|
||||||
|
|
||||||
|
## Missing Methods Implementation
|
||||||
|
- [ ] Implement download_all_workouts method
|
||||||
|
- [ ] Implement reanalyze_all_workouts method
|
||||||
|
- [ ] Implement estimate_cog_from_cadence method
|
||||||
|
- [ ] Implement get_user_cog_confirmation method
|
||||||
|
- [ ] Fix chart generation issue
|
||||||
|
|
||||||
|
## Testing & Validation
|
||||||
|
- [ ] Test with 38T chainring data
|
||||||
|
- [ ] Test with 46T chainring data
|
||||||
|
- [ ] Verify power calculations accuracy
|
||||||
|
- [ ] Validate report generation
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
- [ ] Add data validation and error handling
|
||||||
|
- [ ] Update documentation
|
||||||
|
- [ ] Add logging for chainring detection
|
||||||
Reference in New Issue
Block a user