82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""Core data models for the extract stage.
|
|
|
|
ParsedActivity is the internal representation produced by parsers.
|
|
It gets fed into metrics computation and the BAS JSON writer.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
# Any timestamp before this is almost certainly an uninitialised sensor value
|
|
# (epoch 0, FIT "no-data" sentinel, RTC not yet synced, etc.).
|
|
_MIN_TIMESTAMP = datetime(2000, 1, 1, tzinfo=timezone.utc)
|
|
|
|
|
|
def strip_bogus_leading_points(points: list["DataPoint"]) -> list["DataPoint"]:
|
|
"""Drop leading points whose timestamp predates the year 2000.
|
|
|
|
FIT files occasionally emit a record with timestamp=0 (or another
|
|
pre-2000 value) as an uninitialised sentinel before the real data
|
|
begins. Keeping such a point as points[0] produces a 1970 start
|
|
time and an absurdly large duration_s.
|
|
"""
|
|
i = 0
|
|
while i < len(points) and points[i].timestamp < _MIN_TIMESTAMP:
|
|
i += 1
|
|
return points[i:]
|
|
|
|
|
|
@dataclass
|
|
class DataPoint:
|
|
"""One measurement sample from a GPS/sensor recording."""
|
|
|
|
timestamp: datetime
|
|
lat: Optional[float] = None
|
|
lon: Optional[float] = None
|
|
elevation_m: Optional[float] = None
|
|
hr_bpm: Optional[int] = None
|
|
cadence_rpm: Optional[int] = None
|
|
# Speed from device (km/h). May be absent; we compute it from GPS if so.
|
|
speed_kmh: Optional[float] = None
|
|
power_w: Optional[int] = None
|
|
temperature_c: Optional[float] = None
|
|
# Cumulative distance from device (metres), if recorded.
|
|
distance_m: Optional[float] = None
|
|
|
|
|
|
@dataclass
|
|
class LapData:
|
|
index: int
|
|
started_at: datetime
|
|
duration_s: Optional[int] = None
|
|
distance_m: Optional[float] = None
|
|
elevation_gain_m: Optional[float] = None
|
|
avg_speed_kmh: Optional[float] = None
|
|
avg_hr_bpm: Optional[int] = None
|
|
avg_power_w: Optional[int] = None
|
|
|
|
|
|
@dataclass
|
|
class ParsedActivity:
|
|
"""Raw activity data as produced by a parser, before metric computation."""
|
|
|
|
points: list[DataPoint]
|
|
sport: str # normalised to BAS sport enum
|
|
started_at: datetime
|
|
source_file: str # basename of original file
|
|
source_hash: str # "sha256:{hex}"
|
|
|
|
sub_sport: Optional[str] = None
|
|
device: Optional[str] = None
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
gear: Optional[str] = None
|
|
strava_id: Optional[str] = None
|
|
privacy: Optional[str] = None # "public", "private", or None (caller decides)
|
|
laps: list[LapData] = field(default_factory=list)
|
|
# "barometric" = device has a barometric altimeter (FIT enhanced_altitude present)
|
|
# "gps" = altitude derived from GPS triangulation (GPX, TCX, FIT altitude-only)
|
|
# "unknown" = source not detected (treated as gps for threshold purposes)
|
|
altitude_source: str = "unknown"
|