mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-10 08:17:59 -05:00
Additional activity fields and fit parsing
[backend] added additional columns to the activities table (max_speed, max_power, normalized_power, average_hr, max_hr, average_cad, max_cad) [backend] optimised fit file parse logic [backend] moved backend log to specific logs folder [README] updated README file with new logs folder
This commit is contained in:
@@ -169,7 +169,7 @@ It is recommended to configure the following volumes for code and data persisten
|
||||
| /app | <local_path>/endurain/backend/app:/app | Configure volume if you want to edit the code locally by cloning the repo and comment next line |
|
||||
| /app/user_images | <local_path>/endurain/backend/app/user_images:/app/user_images | Necessary for user image persistence on container image updates |
|
||||
| /app/bulk_import | <local_path>/endurain/backend/bulk_import:/app/bulk_import | Necessary to enable bulk import of activities. Place here your activities files |
|
||||
| /app/app.log | <local_path>/endurain/backend/app.log:/app/app.log | Log file for the backend |
|
||||
| /app/logs | <local_path>/endurain/backend/app.log:/app/logs | Log files for the backend |
|
||||
|
||||
## Bulk import
|
||||
|
||||
|
||||
@@ -464,7 +464,14 @@ def create_activity(activity: activities_schema.Activity, db: Session):
|
||||
elevation_loss=activity.elevation_loss,
|
||||
pace=activity.pace,
|
||||
average_speed=activity.average_speed,
|
||||
max_speed=activity.max_speed,
|
||||
average_power=activity.average_power,
|
||||
max_power=activity.max_power,
|
||||
normalized_power=activity.normalized_power,
|
||||
average_hr=activity.average_hr,
|
||||
max_hr=activity.max_hr,
|
||||
average_cad=activity.average_cad,
|
||||
max_cad=activity.max_cad,
|
||||
calories=activity.calories,
|
||||
visibility=activity.visibility,
|
||||
gear_id=activity.gear_id,
|
||||
|
||||
@@ -14,11 +14,18 @@ class Activity(BaseModel):
|
||||
town: str | None = None
|
||||
country: str | None = None
|
||||
created_at: str | None = None
|
||||
elevation_gain: int
|
||||
elevation_loss: int
|
||||
pace: float
|
||||
average_speed: float
|
||||
average_power: int
|
||||
elevation_gain: int | None = None
|
||||
elevation_loss: int | None = None
|
||||
pace: float | None = None
|
||||
average_speed: float | None = None
|
||||
max_speed: float | None = None
|
||||
average_power: int | None = None
|
||||
max_power: int | None = None
|
||||
normalized_power: int | None = None
|
||||
average_hr: int | None = None
|
||||
max_hr: int | None = None
|
||||
average_cad: int | None = None
|
||||
max_cad: int | None = None
|
||||
calories: int | None = None
|
||||
visibility: int | None = None
|
||||
gear_id: int | None = None
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Added additional columns to activities table
|
||||
|
||||
Revision ID: 24e2676546b1
|
||||
Revises: ab815ee3beae
|
||||
Create Date: 2024-09-17 15:41:59.800993
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '24e2676546b1'
|
||||
down_revision: Union[str, None] = 'ab815ee3beae'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('activities', sa.Column('max_speed', sa.DECIMAL(precision=20, scale=10), nullable=True, comment='Max speed seconds per meter (s/m)'))
|
||||
op.add_column('activities', sa.Column('max_power', sa.Integer(), nullable=True, comment='Max power (watts)'))
|
||||
op.add_column('activities', sa.Column('normalized_power', sa.Integer(), nullable=True, comment='Normalized power (watts)'))
|
||||
op.add_column('activities', sa.Column('average_hr', sa.Integer(), nullable=True, comment='Average heart rate (bpm)'))
|
||||
op.add_column('activities', sa.Column('max_hr', sa.Integer(), nullable=True, comment='Max heart rate (bpm)'))
|
||||
op.add_column('activities', sa.Column('average_cad', sa.Integer(), nullable=True, comment='Average cadence (rpm)'))
|
||||
op.add_column('activities', sa.Column('max_cad', sa.Integer(), nullable=True, comment='Max cadence (rpm)'))
|
||||
op.alter_column('activities', 'elevation_gain',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True,
|
||||
existing_comment='Elevation gain in meters')
|
||||
op.alter_column('activities', 'elevation_loss',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True,
|
||||
existing_comment='Elevation loss in meters')
|
||||
op.alter_column('activities', 'pace',
|
||||
existing_type=mysql.DECIMAL(precision=20, scale=10),
|
||||
nullable=True,
|
||||
existing_comment='Pace seconds per meter (s/m)')
|
||||
op.alter_column('activities', 'average_speed',
|
||||
existing_type=mysql.DECIMAL(precision=20, scale=10),
|
||||
nullable=True,
|
||||
existing_comment='Average speed seconds per meter (s/m)')
|
||||
op.alter_column('activities', 'average_power',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True,
|
||||
existing_comment='Average power (watts)')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('activities', 'average_power',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False,
|
||||
existing_comment='Average power (watts)')
|
||||
op.alter_column('activities', 'average_speed',
|
||||
existing_type=mysql.DECIMAL(precision=20, scale=10),
|
||||
nullable=False,
|
||||
existing_comment='Average speed seconds per meter (s/m)')
|
||||
op.alter_column('activities', 'pace',
|
||||
existing_type=mysql.DECIMAL(precision=20, scale=10),
|
||||
nullable=False,
|
||||
existing_comment='Pace seconds per meter (s/m)')
|
||||
op.alter_column('activities', 'elevation_loss',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False,
|
||||
existing_comment='Elevation loss in meters')
|
||||
op.alter_column('activities', 'elevation_gain',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False,
|
||||
existing_comment='Elevation gain in meters')
|
||||
op.drop_column('activities', 'max_cad')
|
||||
op.drop_column('activities', 'average_cad')
|
||||
op.drop_column('activities', 'max_hr')
|
||||
op.drop_column('activities', 'average_hr')
|
||||
op.drop_column('activities', 'normalized_power')
|
||||
op.drop_column('activities', 'max_power')
|
||||
op.drop_column('activities', 'max_speed')
|
||||
# ### end Alembic commands ###
|
||||
@@ -14,26 +14,20 @@ logger = logging.getLogger("myLogger")
|
||||
|
||||
def parse_activity_streams_from_fit_file(parsed_info: dict, activity_id: int):
|
||||
# Create a list of tuples containing stream type, is_set, and waypoints
|
||||
stream_data_list = [
|
||||
(1, parsed_info["is_heart_rate_set"], parsed_info["hr_waypoints"]),
|
||||
(2, parsed_info["is_power_set"], parsed_info["power_waypoints"]),
|
||||
(3, parsed_info["is_cadence_set"], parsed_info["cad_waypoints"]),
|
||||
(4, parsed_info["is_elevation_set"], parsed_info["ele_waypoints"]),
|
||||
(5, parsed_info["is_velocity_set"], parsed_info["vel_waypoints"]),
|
||||
(6, parsed_info["is_velocity_set"], parsed_info["pace_waypoints"]),
|
||||
(
|
||||
7,
|
||||
parsed_info["prev_latitude"] is not None
|
||||
and parsed_info["prev_longitude"] is not None,
|
||||
parsed_info["lat_lon_waypoints"],
|
||||
),
|
||||
]
|
||||
stream_mapping = {
|
||||
1: ("is_heart_rate_set", "hr_waypoints"),
|
||||
2: ("is_power_set", "power_waypoints"),
|
||||
3: ("is_cadence_set", "cad_waypoints"),
|
||||
4: ("is_elevation_set", "ele_waypoints"),
|
||||
5: ("is_velocity_set", "vel_waypoints"),
|
||||
6: ("is_velocity_set", "pace_waypoints"),
|
||||
7: ("lat_lon_waypoints", "lat_lon_waypoints"),
|
||||
}
|
||||
|
||||
# Filter the list to include only those with is_set True
|
||||
stream_data_list = [
|
||||
(stream_type, is_set, waypoints)
|
||||
for stream_type, is_set, waypoints in stream_data_list
|
||||
if is_set
|
||||
(stream_type, parsed_info[is_set_key], parsed_info[waypoints_key])
|
||||
for stream_type, (is_set_key, waypoints_key) in stream_mapping.items()
|
||||
if parsed_info[is_set_key]
|
||||
]
|
||||
|
||||
# Return activity streams as a list of ActivityStreams objects
|
||||
@@ -51,20 +45,31 @@ def parse_activity_streams_from_fit_file(parsed_info: dict, activity_id: int):
|
||||
def parse_fit_file(file: str, user_id: int) -> dict:
|
||||
# Open the FIT file
|
||||
try:
|
||||
fit_data = fitdecode.FitReader(open(file, "rb"))
|
||||
|
||||
# Initialize default values for various variables
|
||||
activity_name = "Workout"
|
||||
initial_latitude = None
|
||||
initial_longitude = None
|
||||
activity_type = "Workout"
|
||||
distance = 0
|
||||
calories = None
|
||||
distance = None
|
||||
avg_hr = None
|
||||
max_hr = None
|
||||
avg_cadence = None
|
||||
max_cadence = None
|
||||
first_waypoint_time = None
|
||||
last_waypoint_time = None
|
||||
avg_power = None
|
||||
max_power = None
|
||||
ele_gain = None
|
||||
ele_loss = None
|
||||
np = None
|
||||
avg_speed = None
|
||||
max_speed = None
|
||||
activity_name = "Workout"
|
||||
|
||||
city = None
|
||||
town = None
|
||||
country = None
|
||||
process_one_time_fields = 0
|
||||
pace = 0
|
||||
calories = 0
|
||||
visibility = 0
|
||||
|
||||
# Arrays to store waypoint data
|
||||
@@ -86,218 +91,152 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
||||
is_cadence_set = False
|
||||
is_velocity_set = False
|
||||
|
||||
# Iterate over FIT messages
|
||||
for frame in fit_data:
|
||||
if isinstance(frame, fitdecode.FitDataMessage):
|
||||
if frame.name == "session":
|
||||
# Extract calories
|
||||
try:
|
||||
calories = (
|
||||
frame.get_value("total_calories")
|
||||
if frame.get_value("total_calories")
|
||||
else None
|
||||
)
|
||||
except KeyError:
|
||||
calories = 0
|
||||
|
||||
# Extract activity type from sport field
|
||||
try:
|
||||
activity_type = (
|
||||
frame.get_value("sport")
|
||||
if frame.get_value("sport")
|
||||
else "Workout"
|
||||
)
|
||||
# Extract sub-sport
|
||||
with open(file, "rb") as fit_file:
|
||||
fit_data = fitdecode.FitReader(fit_file)
|
||||
|
||||
# Iterate over FIT messages
|
||||
for frame in fit_data:
|
||||
if isinstance(frame, fitdecode.FitDataMessage):
|
||||
if frame.name == "session":
|
||||
# Extract session data
|
||||
(
|
||||
first_waypoint_time,
|
||||
initial_latitude,
|
||||
initial_longitude,
|
||||
activity_type,
|
||||
calories,
|
||||
distance,
|
||||
avg_hr,
|
||||
max_hr,
|
||||
avg_cadence,
|
||||
max_cadence,
|
||||
avg_power,
|
||||
max_power,
|
||||
ele_gain,
|
||||
ele_loss,
|
||||
np,
|
||||
avg_speed,
|
||||
max_speed,
|
||||
) = parse_frame_session(frame)
|
||||
|
||||
# If initial latitude and longitude are set, use them to get city, town, and country
|
||||
if (
|
||||
frame.get_value("sub_sport")
|
||||
and frame.get_value("sub_sport") != "generic"
|
||||
initial_latitude is not None
|
||||
and initial_longitude is not None
|
||||
):
|
||||
if(activity_type == "cycling"):
|
||||
if(frame.get_value("sub_sport") == "virtual_activity"):
|
||||
activity_type = "virtual_ride"
|
||||
else:
|
||||
activity_type = frame.get_value("sub_sport")
|
||||
else:
|
||||
activity_type = frame.get_value("sub_sport")
|
||||
except KeyError:
|
||||
activity_type = "Workout"
|
||||
# Extract activity name
|
||||
if frame.name == "workout":
|
||||
activity_name = (
|
||||
frame.get_value("wkt_name")
|
||||
if frame.get_value("wkt_name")
|
||||
else "Workout"
|
||||
)
|
||||
# Use geocoding API to get city, town, and country based on coordinates
|
||||
location_data = (
|
||||
activities_utils.location_based_on_coordinates(
|
||||
initial_latitude, initial_longitude
|
||||
)
|
||||
)
|
||||
|
||||
# Extract waypoint data
|
||||
if frame.name == "record":
|
||||
# for field in frame.fields:
|
||||
# print(f"{field.name}: {field.value}")
|
||||
# Extract latitude and longitude
|
||||
latitude = frame.get_value("position_lat") or 0
|
||||
longitude = frame.get_value("position_long") or 0
|
||||
# Extract city, town, and country from location data
|
||||
if location_data:
|
||||
city = location_data["city"]
|
||||
town = location_data["town"]
|
||||
country = location_data["country"]
|
||||
|
||||
# Extract elevation
|
||||
try:
|
||||
elevation = frame.get_value("enhanced_altitude")
|
||||
except KeyError:
|
||||
elevation = 0
|
||||
|
||||
# Extract timestamp
|
||||
time = (
|
||||
datetime.fromisoformat(
|
||||
frame.get_value("timestamp").strftime("%Y-%m-%dT%H:%M:%S")
|
||||
# Extract activity name
|
||||
if frame.name == "workout":
|
||||
activity_name = parse_frame_workout(frame)
|
||||
|
||||
# Extract waypoint data
|
||||
if frame.name == "record":
|
||||
# for field in frame.fields:
|
||||
# print(f"{field.name}: {field.value}")
|
||||
# Extract latitude and longitude
|
||||
(
|
||||
latitude,
|
||||
longitude,
|
||||
elevation,
|
||||
time,
|
||||
heart_rate,
|
||||
cadence,
|
||||
power,
|
||||
) = parse_frame_record(frame)
|
||||
|
||||
# Check elevation
|
||||
if elevation is not None:
|
||||
is_elevation_set = True
|
||||
|
||||
# Check if heart rate, cadence, power are set
|
||||
if heart_rate is not None:
|
||||
is_heart_rate_set = True
|
||||
|
||||
if cadence is not None:
|
||||
is_cadence_set = True
|
||||
|
||||
if power is not None:
|
||||
is_power_set = True
|
||||
|
||||
instant_speed = None
|
||||
# Calculate instant speed, pace, and update waypoint arrays
|
||||
if latitude is not None and longitude is not None:
|
||||
instant_speed = activities_utils.calculate_instant_speed(
|
||||
last_waypoint_time,
|
||||
time,
|
||||
latitude,
|
||||
longitude,
|
||||
prev_latitude,
|
||||
prev_longitude,
|
||||
)
|
||||
|
||||
# Calculate instance pace
|
||||
instant_pace = None
|
||||
if instant_speed:
|
||||
instant_pace = 1 / instant_speed
|
||||
is_velocity_set = True
|
||||
|
||||
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
# Append waypoint data to respective arrays
|
||||
if latitude is not None and longitude is not None:
|
||||
lat_lon_waypoints.append(
|
||||
{
|
||||
"time": timestamp,
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
}
|
||||
)
|
||||
|
||||
append_if_not_none(ele_waypoints, timestamp, elevation, "ele")
|
||||
append_if_not_none(hr_waypoints, timestamp, heart_rate, "hr")
|
||||
append_if_not_none(cad_waypoints, timestamp, cadence, "cad")
|
||||
append_if_not_none(power_waypoints, timestamp, power, "power")
|
||||
append_if_not_none(
|
||||
vel_waypoints, timestamp, instant_speed, "vel"
|
||||
)
|
||||
if frame.get_value("timestamp")
|
||||
else ""
|
||||
)
|
||||
|
||||
# Extract heart rate
|
||||
try:
|
||||
heart_rate = frame.get_value("heart_rate")
|
||||
except KeyError:
|
||||
heart_rate = 0
|
||||
|
||||
# Extract cadence
|
||||
try:
|
||||
cadence = frame.get_value("cadence")
|
||||
except KeyError:
|
||||
cadence = 0
|
||||
|
||||
# Extract power
|
||||
try:
|
||||
power = frame.get_value("power")
|
||||
except KeyError:
|
||||
power = 0
|
||||
|
||||
# Convert FIT coordinates to degrees if available
|
||||
if latitude is not None and longitude is not None:
|
||||
latitude = latitude * (180 / 2**31)
|
||||
longitude = longitude * (180 / 2**31)
|
||||
|
||||
if prev_latitude is not None and prev_longitude is not None and latitude != 0 and longitude != 0 and prev_latitude != 0 and prev_longitude != 0:
|
||||
distance += activities_utils.calculate_distance(
|
||||
prev_latitude, prev_longitude, latitude, longitude
|
||||
append_if_not_none(
|
||||
pace_waypoints, timestamp, instant_pace, "pace"
|
||||
)
|
||||
|
||||
# Check elevation
|
||||
if elevation is not None and elevation != 0:
|
||||
is_elevation_set = True
|
||||
|
||||
if first_waypoint_time is None:
|
||||
first_waypoint_time = time
|
||||
|
||||
if process_one_time_fields == 0 and latitude and longitude:
|
||||
# Use geocoding API to get city, town, and country based on coordinates
|
||||
location_data = activities_utils.location_based_on_coordinates(
|
||||
latitude, longitude
|
||||
# Update previous latitude, longitude, and last waypoint time
|
||||
prev_latitude, prev_longitude, last_waypoint_time = (
|
||||
latitude,
|
||||
longitude,
|
||||
time,
|
||||
)
|
||||
|
||||
if location_data:
|
||||
city = location_data["city"]
|
||||
town = location_data["town"]
|
||||
country = location_data["country"]
|
||||
|
||||
process_one_time_fields = 1
|
||||
|
||||
# Check if heart rate, cadence, power are set
|
||||
if heart_rate != 0:
|
||||
is_heart_rate_set = True
|
||||
|
||||
if cadence != 0:
|
||||
is_cadence_set = True
|
||||
|
||||
if power != 0:
|
||||
is_power_set = True
|
||||
|
||||
# Calculate instant speed, pace, and update waypoint arrays
|
||||
instant_speed = activities_utils.calculate_instant_speed(
|
||||
last_waypoint_time,
|
||||
time,
|
||||
latitude,
|
||||
longitude,
|
||||
prev_latitude,
|
||||
prev_longitude,
|
||||
)
|
||||
|
||||
# Calculate instance pace
|
||||
instant_pace = 0
|
||||
if instant_speed > 0:
|
||||
instant_pace = 1 / instant_speed
|
||||
is_velocity_set = True
|
||||
|
||||
# Append waypoint data to respective arrays
|
||||
if latitude is not None and longitude is not None:
|
||||
lat_lon_waypoints.append(
|
||||
{
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
}
|
||||
)
|
||||
|
||||
if elevation is not None:
|
||||
ele_waypoints.append(
|
||||
{
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"ele": elevation,
|
||||
}
|
||||
)
|
||||
|
||||
if heart_rate is not None:
|
||||
hr_waypoints.append(
|
||||
{
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"hr": heart_rate,
|
||||
}
|
||||
)
|
||||
|
||||
if cadence is not None:
|
||||
cad_waypoints.append(
|
||||
{"time": time.strftime("%Y-%m-%dT%H:%M:%S"), "cad": cadence}
|
||||
)
|
||||
|
||||
if power is not None:
|
||||
power_waypoints.append(
|
||||
{"time": time.strftime("%Y-%m-%dT%H:%M:%S"), "power": power}
|
||||
)
|
||||
|
||||
if instant_speed is not None and instant_speed != 0:
|
||||
vel_waypoints.append(
|
||||
{
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"vel": instant_speed,
|
||||
}
|
||||
)
|
||||
|
||||
if instant_pace != 0:
|
||||
pace_waypoints.append(
|
||||
{
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"pace": instant_pace,
|
||||
}
|
||||
)
|
||||
|
||||
# Update previous latitude, longitude, and last waypoint time
|
||||
prev_latitude, prev_longitude, last_waypoint_time = (
|
||||
latitude,
|
||||
longitude,
|
||||
time,
|
||||
)
|
||||
|
||||
# Calculate elevation gain/loss, pace, average speed, and average power
|
||||
elevation_data = activities_utils.calculate_elevation_gain_loss(ele_waypoints)
|
||||
elevation_gain = elevation_data["elevation_gain"]
|
||||
elevation_loss = elevation_data["elevation_loss"]
|
||||
if ele_gain is None and ele_loss is None:
|
||||
elevation_data = activities_utils.calculate_elevation_gain_loss(
|
||||
ele_waypoints
|
||||
)
|
||||
ele_gain = elevation_data["elevation_gain"]
|
||||
ele_loss = elevation_data["elevation_loss"]
|
||||
|
||||
pace = activities_utils.calculate_pace(
|
||||
distance, first_waypoint_time, last_waypoint_time
|
||||
)
|
||||
|
||||
average_speed = activities_utils.calculate_average_speed(
|
||||
distance, first_waypoint_time, last_waypoint_time
|
||||
)
|
||||
if avg_speed is None:
|
||||
avg_speed = activities_utils.calculate_average_speed(
|
||||
distance, first_waypoint_time, last_waypoint_time
|
||||
)
|
||||
|
||||
average_power = activities_utils.calculate_average_power(power_waypoints)
|
||||
if avg_power is None:
|
||||
avg_power = activities_utils.calculate_average_power(power_waypoints)
|
||||
|
||||
# Create an Activity object with parsed data
|
||||
activity = activities_schema.Activity(
|
||||
@@ -310,11 +249,18 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
||||
city=city,
|
||||
town=town,
|
||||
country=country,
|
||||
elevation_gain=elevation_gain,
|
||||
elevation_loss=elevation_loss,
|
||||
elevation_gain=ele_gain,
|
||||
elevation_loss=ele_loss,
|
||||
pace=pace,
|
||||
average_speed=average_speed,
|
||||
average_power=average_power,
|
||||
average_speed=avg_speed,
|
||||
max_speed=max_speed,
|
||||
average_power=avg_power,
|
||||
max_power=max_power,
|
||||
normalized_power=np,
|
||||
average_hr=avg_hr,
|
||||
max_hr=max_hr,
|
||||
average_cad=avg_cadence,
|
||||
max_cad=max_cadence,
|
||||
calories=calories,
|
||||
visibility=visibility,
|
||||
strava_gear_id=None,
|
||||
@@ -339,6 +285,14 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
||||
"prev_latitude": prev_latitude,
|
||||
"prev_longitude": prev_longitude,
|
||||
}
|
||||
except (FileNotFoundError, fitdecode.FitDecodeError) as err:
|
||||
# Log the exception
|
||||
logger.error(f"File error in parse_fit_file: {err}", exc_info=True)
|
||||
# Raise an HTTPException with a 400 Bad Request status code
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Can't open FIT file",
|
||||
) from err
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
logger.error(f"Error in parse_fit_file: {err}", exc_info=True)
|
||||
@@ -347,3 +301,107 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Can't parse FIT file",
|
||||
) from err
|
||||
|
||||
|
||||
def parse_frame_session(frame):
|
||||
# Extracting data using the helper function
|
||||
start_time = get_value_from_frame(frame, "start_time")
|
||||
initial_latitude = get_value_from_frame(frame, "start_position_lat")
|
||||
initial_longitude = get_value_from_frame(frame, "start_position_long")
|
||||
|
||||
# Activity type logic
|
||||
activity_type = get_value_from_frame(frame, "sport", "Workout")
|
||||
sub_sport = get_value_from_frame(frame, "sub_sport")
|
||||
if sub_sport and sub_sport != "generic":
|
||||
if activity_type == "cycling" and sub_sport == "virtual_activity":
|
||||
activity_type = "virtual_ride"
|
||||
else:
|
||||
activity_type = sub_sport
|
||||
|
||||
# Extracting other values with default fallback
|
||||
calories = get_value_from_frame(frame, "total_calories")
|
||||
distance = get_value_from_frame(frame, "total_distance")
|
||||
avg_hr = get_value_from_frame(frame, "avg_heart_rate")
|
||||
max_hr = get_value_from_frame(frame, "max_heart_rate")
|
||||
avg_cadence = get_value_from_frame(frame, "avg_cadence")
|
||||
max_cadence = get_value_from_frame(frame, "max_cadence")
|
||||
avg_power = get_value_from_frame(frame, "avg_power")
|
||||
max_power = get_value_from_frame(frame, "max_power")
|
||||
ele_gain = get_value_from_frame(frame, "total_ascent")
|
||||
ele_loss = get_value_from_frame(frame, "total_descent")
|
||||
np = get_value_from_frame(frame, "normalized_power")
|
||||
avg_speed = get_value_from_frame(frame, "enhanced_avg_speed")
|
||||
max_speed = get_value_from_frame(frame, "enhanced_max_speed")
|
||||
|
||||
initial_latitude, initial_longitude = convert_coordinates_to_degrees(
|
||||
initial_latitude, initial_longitude
|
||||
)
|
||||
|
||||
# Return all extracted values
|
||||
return (
|
||||
start_time,
|
||||
initial_latitude,
|
||||
initial_longitude,
|
||||
activity_type,
|
||||
calories,
|
||||
distance,
|
||||
avg_hr,
|
||||
max_hr,
|
||||
avg_cadence,
|
||||
max_cadence,
|
||||
avg_power,
|
||||
max_power,
|
||||
ele_gain,
|
||||
ele_loss,
|
||||
np,
|
||||
avg_speed,
|
||||
max_speed,
|
||||
)
|
||||
|
||||
|
||||
def parse_frame_workout(frame):
|
||||
# Extracting data using the helper function
|
||||
name = get_value_from_frame(frame, "wkt_name", "Workout")
|
||||
|
||||
# Return the extracted name
|
||||
return name
|
||||
|
||||
|
||||
def parse_frame_record(frame):
|
||||
# Extracting data using the helper function
|
||||
latitude = get_value_from_frame(frame, "position_lat")
|
||||
longitude = get_value_from_frame(frame, "position_long")
|
||||
elevation = get_value_from_frame(frame, "enhanced_altitude")
|
||||
time = get_value_from_frame(frame, "timestamp")
|
||||
if time:
|
||||
time = datetime.fromisoformat(time.strftime("%Y-%m-%dT%H:%M:%S"))
|
||||
heart_rate = get_value_from_frame(frame, "heart_rate")
|
||||
cadence = get_value_from_frame(frame, "cadence")
|
||||
power = get_value_from_frame(frame, "power")
|
||||
|
||||
latitude, longitude = convert_coordinates_to_degrees(latitude, longitude)
|
||||
|
||||
# Return all extracted values
|
||||
return latitude, longitude, elevation, time, heart_rate, cadence, power
|
||||
|
||||
|
||||
def get_value_from_frame(frame, key, default=None):
|
||||
try:
|
||||
value = frame.get_value(key)
|
||||
return value if value else default
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
|
||||
def convert_coordinates_to_degrees(latitude, longitude):
|
||||
# Convert FIT coordinates to degrees if available
|
||||
if latitude is not None and longitude is not None:
|
||||
latitude = latitude * (180 / 2**31)
|
||||
longitude = longitude * (180 / 2**31)
|
||||
|
||||
return latitude, longitude
|
||||
|
||||
|
||||
def append_if_not_none(waypoint_list, time, value, key):
|
||||
if value is not None:
|
||||
waypoint_list.append({"time": time, key: value})
|
||||
|
||||
0
backend/app/logs/__init__.py
Normal file
0
backend/app/logs/__init__.py
Normal file
@@ -78,7 +78,7 @@ def retrieve_strava_user_activities_for_last_day():
|
||||
logger = logging.getLogger("myLogger")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
file_handler = logging.FileHandler("app.log")
|
||||
file_handler = logging.FileHandler("logs/app.log")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
|
||||
@@ -241,19 +241,30 @@ class Activity(Base):
|
||||
created_at = Column(
|
||||
DateTime, nullable=False, comment="Activity creation date (datetime)"
|
||||
)
|
||||
elevation_gain = Column(Integer, nullable=False, comment="Elevation gain in meters")
|
||||
elevation_loss = Column(Integer, nullable=False, comment="Elevation loss in meters")
|
||||
elevation_gain = Column(Integer, nullable=True, comment="Elevation gain in meters")
|
||||
elevation_loss = Column(Integer, nullable=True, comment="Elevation loss in meters")
|
||||
pace = Column(
|
||||
DECIMAL(precision=20, scale=10),
|
||||
nullable=False,
|
||||
nullable=True,
|
||||
comment="Pace seconds per meter (s/m)",
|
||||
)
|
||||
average_speed = Column(
|
||||
DECIMAL(precision=20, scale=10),
|
||||
nullable=False,
|
||||
nullable=True,
|
||||
comment="Average speed seconds per meter (s/m)",
|
||||
)
|
||||
average_power = Column(Integer, nullable=False, comment="Average power (watts)")
|
||||
max_speed = Column(
|
||||
DECIMAL(precision=20, scale=10),
|
||||
nullable=True,
|
||||
comment="Max speed seconds per meter (s/m)",
|
||||
)
|
||||
average_power = Column(Integer, nullable=True, comment="Average power (watts)")
|
||||
max_power = Column(Integer, nullable=True, comment="Max power (watts)")
|
||||
normalized_power = Column(Integer, nullable=True, comment="Normalized power (watts)")
|
||||
average_hr = Column(Integer, nullable=True, comment="Average heart rate (bpm)")
|
||||
max_hr = Column(Integer, nullable=True, comment="Max heart rate (bpm)")
|
||||
average_cad = Column(Integer, nullable=True, comment="Average cadence (rpm)")
|
||||
max_cad = Column(Integer, nullable=True, comment="Max cadence (rpm)")
|
||||
calories = Column(
|
||||
Integer,
|
||||
nullable=True,
|
||||
|
||||
Reference in New Issue
Block a user