mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57: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 | <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/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/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
|
## Bulk import
|
||||||
|
|
||||||
|
|||||||
@@ -464,7 +464,14 @@ def create_activity(activity: activities_schema.Activity, db: Session):
|
|||||||
elevation_loss=activity.elevation_loss,
|
elevation_loss=activity.elevation_loss,
|
||||||
pace=activity.pace,
|
pace=activity.pace,
|
||||||
average_speed=activity.average_speed,
|
average_speed=activity.average_speed,
|
||||||
|
max_speed=activity.max_speed,
|
||||||
average_power=activity.average_power,
|
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,
|
calories=activity.calories,
|
||||||
visibility=activity.visibility,
|
visibility=activity.visibility,
|
||||||
gear_id=activity.gear_id,
|
gear_id=activity.gear_id,
|
||||||
|
|||||||
@@ -14,11 +14,18 @@ class Activity(BaseModel):
|
|||||||
town: str | None = None
|
town: str | None = None
|
||||||
country: str | None = None
|
country: str | None = None
|
||||||
created_at: str | None = None
|
created_at: str | None = None
|
||||||
elevation_gain: int
|
elevation_gain: int | None = None
|
||||||
elevation_loss: int
|
elevation_loss: int | None = None
|
||||||
pace: float
|
pace: float | None = None
|
||||||
average_speed: float
|
average_speed: float | None = None
|
||||||
average_power: int
|
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
|
calories: int | None = None
|
||||||
visibility: int | None = None
|
visibility: int | None = None
|
||||||
gear_id: 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):
|
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
|
# Create a list of tuples containing stream type, is_set, and waypoints
|
||||||
stream_data_list = [
|
stream_mapping = {
|
||||||
(1, parsed_info["is_heart_rate_set"], parsed_info["hr_waypoints"]),
|
1: ("is_heart_rate_set", "hr_waypoints"),
|
||||||
(2, parsed_info["is_power_set"], parsed_info["power_waypoints"]),
|
2: ("is_power_set", "power_waypoints"),
|
||||||
(3, parsed_info["is_cadence_set"], parsed_info["cad_waypoints"]),
|
3: ("is_cadence_set", "cad_waypoints"),
|
||||||
(4, parsed_info["is_elevation_set"], parsed_info["ele_waypoints"]),
|
4: ("is_elevation_set", "ele_waypoints"),
|
||||||
(5, parsed_info["is_velocity_set"], parsed_info["vel_waypoints"]),
|
5: ("is_velocity_set", "vel_waypoints"),
|
||||||
(6, parsed_info["is_velocity_set"], parsed_info["pace_waypoints"]),
|
6: ("is_velocity_set", "pace_waypoints"),
|
||||||
(
|
7: ("lat_lon_waypoints", "lat_lon_waypoints"),
|
||||||
7,
|
}
|
||||||
parsed_info["prev_latitude"] is not None
|
|
||||||
and parsed_info["prev_longitude"] is not None,
|
|
||||||
parsed_info["lat_lon_waypoints"],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Filter the list to include only those with is_set True
|
|
||||||
stream_data_list = [
|
stream_data_list = [
|
||||||
(stream_type, is_set, waypoints)
|
(stream_type, parsed_info[is_set_key], parsed_info[waypoints_key])
|
||||||
for stream_type, is_set, waypoints in stream_data_list
|
for stream_type, (is_set_key, waypoints_key) in stream_mapping.items()
|
||||||
if is_set
|
if parsed_info[is_set_key]
|
||||||
]
|
]
|
||||||
|
|
||||||
# Return activity streams as a list of ActivityStreams objects
|
# 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:
|
def parse_fit_file(file: str, user_id: int) -> dict:
|
||||||
# Open the FIT file
|
# Open the FIT file
|
||||||
try:
|
try:
|
||||||
fit_data = fitdecode.FitReader(open(file, "rb"))
|
|
||||||
|
|
||||||
# Initialize default values for various variables
|
# Initialize default values for various variables
|
||||||
activity_name = "Workout"
|
initial_latitude = None
|
||||||
|
initial_longitude = None
|
||||||
activity_type = "Workout"
|
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
|
first_waypoint_time = None
|
||||||
last_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
|
city = None
|
||||||
town = None
|
town = None
|
||||||
country = None
|
country = None
|
||||||
process_one_time_fields = 0
|
|
||||||
pace = 0
|
pace = 0
|
||||||
calories = 0
|
|
||||||
visibility = 0
|
visibility = 0
|
||||||
|
|
||||||
# Arrays to store waypoint data
|
# Arrays to store waypoint data
|
||||||
@@ -86,218 +91,152 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
|||||||
is_cadence_set = False
|
is_cadence_set = False
|
||||||
is_velocity_set = False
|
is_velocity_set = False
|
||||||
|
|
||||||
# Iterate over FIT messages
|
with open(file, "rb") as fit_file:
|
||||||
for frame in fit_data:
|
fit_data = fitdecode.FitReader(fit_file)
|
||||||
if isinstance(frame, fitdecode.FitDataMessage):
|
|
||||||
if frame.name == "session":
|
# Iterate over FIT messages
|
||||||
# Extract calories
|
for frame in fit_data:
|
||||||
try:
|
if isinstance(frame, fitdecode.FitDataMessage):
|
||||||
calories = (
|
if frame.name == "session":
|
||||||
frame.get_value("total_calories")
|
# Extract session data
|
||||||
if frame.get_value("total_calories")
|
(
|
||||||
else None
|
first_waypoint_time,
|
||||||
)
|
initial_latitude,
|
||||||
except KeyError:
|
initial_longitude,
|
||||||
calories = 0
|
activity_type,
|
||||||
|
calories,
|
||||||
# Extract activity type from sport field
|
distance,
|
||||||
try:
|
avg_hr,
|
||||||
activity_type = (
|
max_hr,
|
||||||
frame.get_value("sport")
|
avg_cadence,
|
||||||
if frame.get_value("sport")
|
max_cadence,
|
||||||
else "Workout"
|
avg_power,
|
||||||
)
|
max_power,
|
||||||
# Extract sub-sport
|
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 (
|
if (
|
||||||
frame.get_value("sub_sport")
|
initial_latitude is not None
|
||||||
and frame.get_value("sub_sport") != "generic"
|
and initial_longitude is not None
|
||||||
):
|
):
|
||||||
if(activity_type == "cycling"):
|
# Use geocoding API to get city, town, and country based on coordinates
|
||||||
if(frame.get_value("sub_sport") == "virtual_activity"):
|
location_data = (
|
||||||
activity_type = "virtual_ride"
|
activities_utils.location_based_on_coordinates(
|
||||||
else:
|
initial_latitude, initial_longitude
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract waypoint data
|
# Extract city, town, and country from location data
|
||||||
if frame.name == "record":
|
if location_data:
|
||||||
# for field in frame.fields:
|
city = location_data["city"]
|
||||||
# print(f"{field.name}: {field.value}")
|
town = location_data["town"]
|
||||||
# Extract latitude and longitude
|
country = location_data["country"]
|
||||||
latitude = frame.get_value("position_lat") or 0
|
|
||||||
longitude = frame.get_value("position_long") or 0
|
|
||||||
|
|
||||||
# Extract elevation
|
# Extract activity name
|
||||||
try:
|
if frame.name == "workout":
|
||||||
elevation = frame.get_value("enhanced_altitude")
|
activity_name = parse_frame_workout(frame)
|
||||||
except KeyError:
|
|
||||||
elevation = 0
|
# Extract waypoint data
|
||||||
|
if frame.name == "record":
|
||||||
# Extract timestamp
|
# for field in frame.fields:
|
||||||
time = (
|
# print(f"{field.name}: {field.value}")
|
||||||
datetime.fromisoformat(
|
# Extract latitude and longitude
|
||||||
frame.get_value("timestamp").strftime("%Y-%m-%dT%H:%M:%S")
|
(
|
||||||
|
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")
|
append_if_not_none(
|
||||||
else ""
|
pace_waypoints, timestamp, instant_pace, "pace"
|
||||||
)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check elevation
|
# Update previous latitude, longitude, and last waypoint time
|
||||||
if elevation is not None and elevation != 0:
|
prev_latitude, prev_longitude, last_waypoint_time = (
|
||||||
is_elevation_set = True
|
latitude,
|
||||||
|
longitude,
|
||||||
if first_waypoint_time is None:
|
time,
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
# Calculate elevation gain/loss, pace, average speed, and average power
|
||||||
elevation_data = activities_utils.calculate_elevation_gain_loss(ele_waypoints)
|
if ele_gain is None and ele_loss is None:
|
||||||
elevation_gain = elevation_data["elevation_gain"]
|
elevation_data = activities_utils.calculate_elevation_gain_loss(
|
||||||
elevation_loss = elevation_data["elevation_loss"]
|
ele_waypoints
|
||||||
|
)
|
||||||
|
ele_gain = elevation_data["elevation_gain"]
|
||||||
|
ele_loss = elevation_data["elevation_loss"]
|
||||||
|
|
||||||
pace = activities_utils.calculate_pace(
|
pace = activities_utils.calculate_pace(
|
||||||
distance, first_waypoint_time, last_waypoint_time
|
distance, first_waypoint_time, last_waypoint_time
|
||||||
)
|
)
|
||||||
|
|
||||||
average_speed = activities_utils.calculate_average_speed(
|
if avg_speed is None:
|
||||||
distance, first_waypoint_time, last_waypoint_time
|
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
|
# Create an Activity object with parsed data
|
||||||
activity = activities_schema.Activity(
|
activity = activities_schema.Activity(
|
||||||
@@ -310,11 +249,18 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
|||||||
city=city,
|
city=city,
|
||||||
town=town,
|
town=town,
|
||||||
country=country,
|
country=country,
|
||||||
elevation_gain=elevation_gain,
|
elevation_gain=ele_gain,
|
||||||
elevation_loss=elevation_loss,
|
elevation_loss=ele_loss,
|
||||||
pace=pace,
|
pace=pace,
|
||||||
average_speed=average_speed,
|
average_speed=avg_speed,
|
||||||
average_power=average_power,
|
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,
|
calories=calories,
|
||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
strava_gear_id=None,
|
strava_gear_id=None,
|
||||||
@@ -339,6 +285,14 @@ def parse_fit_file(file: str, user_id: int) -> dict:
|
|||||||
"prev_latitude": prev_latitude,
|
"prev_latitude": prev_latitude,
|
||||||
"prev_longitude": prev_longitude,
|
"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:
|
except Exception as err:
|
||||||
# Log the exception
|
# Log the exception
|
||||||
logger.error(f"Error in parse_fit_file: {err}", exc_info=True)
|
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,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Can't parse FIT file",
|
detail="Can't parse FIT file",
|
||||||
) from err
|
) 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 = logging.getLogger("myLogger")
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
file_handler = logging.FileHandler("app.log")
|
file_handler = logging.FileHandler("logs/app.log")
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
|||||||
@@ -241,19 +241,30 @@ class Activity(Base):
|
|||||||
created_at = Column(
|
created_at = Column(
|
||||||
DateTime, nullable=False, comment="Activity creation date (datetime)"
|
DateTime, nullable=False, comment="Activity creation date (datetime)"
|
||||||
)
|
)
|
||||||
elevation_gain = Column(Integer, nullable=False, comment="Elevation gain in meters")
|
elevation_gain = Column(Integer, nullable=True, comment="Elevation gain in meters")
|
||||||
elevation_loss = Column(Integer, nullable=False, comment="Elevation loss in meters")
|
elevation_loss = Column(Integer, nullable=True, comment="Elevation loss in meters")
|
||||||
pace = Column(
|
pace = Column(
|
||||||
DECIMAL(precision=20, scale=10),
|
DECIMAL(precision=20, scale=10),
|
||||||
nullable=False,
|
nullable=True,
|
||||||
comment="Pace seconds per meter (s/m)",
|
comment="Pace seconds per meter (s/m)",
|
||||||
)
|
)
|
||||||
average_speed = Column(
|
average_speed = Column(
|
||||||
DECIMAL(precision=20, scale=10),
|
DECIMAL(precision=20, scale=10),
|
||||||
nullable=False,
|
nullable=True,
|
||||||
comment="Average speed seconds per meter (s/m)",
|
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(
|
calories = Column(
|
||||||
Integer,
|
Integer,
|
||||||
nullable=True,
|
nullable=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user