#!/usr/bin/env python3 """ TCX Parser - Extraherar träningsdata från TCX-filer Skapar kompakt JSON-sammanfattning för analys Användning: python3 parse_tcx.py python3 parse_tcx.py Data/training/activity_12345.tcx Output: Skapar .json med sammanfattad data """ import xml.etree.ElementTree as ET import json import sys from pathlib import Path from datetime import datetime # Namespace definitions för TCX NAMESPACES = { 'tcx': 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2', 'ns3': 'http://www.garmin.com/xmlschemas/ActivityExtension/v2' } def parse_tcx_file(tcx_path): """Parsar TCX-fil och extraherar all relevant data""" tree = ET.parse(tcx_path) root = tree.getroot() # Hitta aktiviteten activity = root.find('.//tcx:Activity', NAMESPACES) if activity is None: raise ValueError("Ingen aktivitet hittades i TCX-filen") # Grundläggande info sport = activity.get('Sport', 'Unknown') activity_id = activity.find('tcx:Id', NAMESPACES).text # Samla data från varv laps = activity.findall('.//tcx:Lap', NAMESPACES) lap_data = [] total_time = 0 total_distance = 0 total_calories = 0 max_speed = 0 for i, lap in enumerate(laps, 1): lap_info = { 'number': i, 'start_time': lap.get('StartTime'), } # Extrahera varvdata time_elem = lap.find('tcx:TotalTimeSeconds', NAMESPACES) dist_elem = lap.find('tcx:DistanceMeters', NAMESPACES) cal_elem = lap.find('tcx:Calories', NAMESPACES) max_speed_elem = lap.find('tcx:MaximumSpeed', NAMESPACES) avg_hr_elem = lap.find('.//tcx:AverageHeartRateBpm/tcx:Value', NAMESPACES) max_hr_elem = lap.find('.//tcx:MaximumHeartRateBpm/tcx:Value', NAMESPACES) avg_cad_elem = lap.find('tcx:Cadence', NAMESPACES) if time_elem is not None: lap_time = float(time_elem.text) lap_info['time_seconds'] = lap_time total_time += lap_time if dist_elem is not None: lap_dist = float(dist_elem.text) lap_info['distance_meters'] = lap_dist total_distance += lap_dist if cal_elem is not None: lap_cal = int(cal_elem.text) lap_info['calories'] = lap_cal total_calories += lap_cal if max_speed_elem is not None: lap_max_speed = float(max_speed_elem.text) lap_info['max_speed_ms'] = lap_max_speed max_speed = max(max_speed, lap_max_speed) if avg_hr_elem is not None: lap_info['avg_hr'] = int(avg_hr_elem.text) if max_hr_elem is not None: lap_info['max_hr'] = int(max_hr_elem.text) if avg_cad_elem is not None: lap_info['avg_cadence'] = int(avg_cad_elem.text) lap_data.append(lap_info) # Samla data från trackpoints trackpoints = root.findall('.//tcx:Trackpoint', NAMESPACES) hr_data = [] power_data = [] cadence_data = [] altitude_data = [] speed_data = [] for tp in trackpoints: # Puls hr_elem = tp.find('.//tcx:HeartRateBpm/tcx:Value', NAMESPACES) if hr_elem is not None: hr_data.append(int(hr_elem.text)) # Effekt power_elem = tp.find('.//ns3:Watts', NAMESPACES) if power_elem is not None: power_data.append(int(power_elem.text)) # Kadans cad_elem = tp.find('tcx:Cadence', NAMESPACES) if cad_elem is not None: cadence_data.append(int(cad_elem.text)) # Höjd alt_elem = tp.find('tcx:AltitudeMeters', NAMESPACES) if alt_elem is not None: altitude_data.append(float(alt_elem.text)) # Hastighet speed_elem = tp.find('.//ns3:Speed', NAMESPACES) if speed_elem is not None: speed_data.append(float(speed_elem.text)) # Beräkna pulsstatistik och zoner hr_stats = None hr_zones = None if hr_data: avg_hr = sum(hr_data) / len(hr_data) min_hr = min(hr_data) max_hr = max(hr_data) # Pulszoner (baserat på 5-zonssystem) # Kan justeras baserat på individuell maxpuls zone1 = sum(1 for hr in hr_data if hr < 110) zone2 = sum(1 for hr in hr_data if 110 <= hr < 129) zone3 = sum(1 for hr in hr_data if 129 <= hr < 147) zone4 = sum(1 for hr in hr_data if 147 <= hr < 166) zone5 = sum(1 for hr in hr_data if hr >= 166) total_points = len(hr_data) hr_stats = { 'avg': round(avg_hr, 1), 'min': min_hr, 'max': max_hr, } hr_zones = { 'zone1_pct': round(zone1 / total_points * 100, 1), 'zone2_pct': round(zone2 / total_points * 100, 1), 'zone3_pct': round(zone3 / total_points * 100, 1), 'zone4_pct': round(zone4 / total_points * 100, 1), 'zone5_pct': round(zone5 / total_points * 100, 1), 'note': 'Zoner baserade på: Z1<110, Z2:110-129, Z3:129-147, Z4:147-166, Z5>166 bpm' } # Beräkna effektstatistik power_stats = None if power_data: power_stats = { 'avg': round(sum(power_data) / len(power_data), 1), 'min': min(power_data), 'max': max(power_data), } # Beräkna kadansstatistik cadence_stats = None if cadence_data: cadence_stats = { 'avg': round(sum(cadence_data) / len(cadence_data), 1), 'min': min(cadence_data), 'max': max(cadence_data), } # Beräkna höjdstatistik altitude_stats = None if altitude_data: elevation_gain = 0 for i in range(1, len(altitude_data)): diff = altitude_data[i] - altitude_data[i-1] if diff > 0: elevation_gain += diff altitude_stats = { 'min': round(min(altitude_data), 1), 'max': round(max(altitude_data), 1), 'gain': round(elevation_gain, 1), } # Beräkna hastighetsstatistik speed_stats = None if speed_data: avg_speed = sum(speed_data) / len(speed_data) speed_stats = { 'avg_ms': round(avg_speed, 2), 'avg_kmh': round(avg_speed * 3.6, 1), 'max_ms': round(max(speed_data), 2), 'max_kmh': round(max(speed_data) * 3.6, 1), } # Bygg sammanfattning summary = { 'file': str(tcx_path), 'parsed_at': datetime.now().isoformat(), 'activity': { 'sport': sport, 'id': activity_id, 'date': activity_id.split('T')[0] if 'T' in activity_id else activity_id, }, 'summary': { 'total_time_seconds': round(total_time, 1), 'total_time_formatted': f"{int(total_time//60)}:{int(total_time%60):02d}", 'total_distance_meters': round(total_distance, 2), 'total_distance_km': round(total_distance / 1000, 2), 'avg_speed_kmh': round((total_distance / total_time) * 3.6, 1) if total_time > 0 else 0, 'max_speed_kmh': round(max_speed * 3.6, 1), 'total_calories': total_calories, 'num_laps': len(laps), 'num_trackpoints': len(trackpoints), }, 'heart_rate': hr_stats, 'heart_rate_zones': hr_zones, 'power': power_stats, 'cadence': cadence_stats, 'altitude': altitude_stats, 'speed': speed_stats, 'laps': lap_data, } return summary def main(): if len(sys.argv) < 2: print("Användning: python3 parse_tcx.py ") print("Exempel: python3 parse_tcx.py Data/training/activity_12345.tcx") sys.exit(1) tcx_file = Path(sys.argv[1]) if not tcx_file.exists(): print(f"Fel: Filen {tcx_file} hittades inte") sys.exit(1) try: print(f"Parsar {tcx_file}...") summary = parse_tcx_file(tcx_file) # Spara som JSON output_file = tcx_file.with_suffix('.tcx.json') with open(output_file, 'w', encoding='utf-8') as f: json.dump(summary, f, indent=2, ensure_ascii=False) print(f"✓ Klar! Sammanfattning sparad i: {output_file}") print(f"\nSnabb sammanfattning:") print(f" Sport: {summary['activity']['sport']}") print(f" Datum: {summary['activity']['date']}") print(f" Tid: {summary['summary']['total_time_formatted']}") print(f" Distans: {summary['summary']['total_distance_km']} km") print(f" Genomsnittshastighet: {summary['summary']['avg_speed_kmh']} km/h") if summary['heart_rate']: print(f" Genomsnittspuls: {summary['heart_rate']['avg']} bpm") print(f" Max puls: {summary['heart_rate']['max']} bpm") if summary['power']: print(f" Genomsnittseffekt: {summary['power']['avg']} W") print(f"\nAnvänd denna JSON-fil för snabb analys istället för stora TCX-filen!") except Exception as e: print(f"Fel vid parsning: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == '__main__': main()