mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 07:47:58 -05:00
commitd32e3d8fdcMerge:d722e793db959207Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Wed Jun 25 12:52:35 2025 +0100 Merge remote-tracking branch 'origin/l10n_pre-release' into pre-release commitdb959207c1Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:51:52 2025 +0100 New translations activitymandabovepillscomponent.json (Portuguese) commit595256934eAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:50:51 2025 +0100 New translations editactivitymodalcomponent.json (Portuguese) commitc659dcd84fAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:50:46 2025 +0100 New translations activitysummarycomponent.json (Portuguese) commit1f8e699753Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:41 2025 +0100 New translations editactivitymodalcomponent.json (Portuguese) commitc93884c2c7Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:40 2025 +0100 New translations editactivitymodalcomponent.json (German) commit9a59c21a75Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:39 2025 +0100 New translations editactivitymodalcomponent.json (Catalan) commit72ea5b5467Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:38 2025 +0100 New translations editactivitymodalcomponent.json (Spanish) commit7f309e9e32Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:37 2025 +0100 New translations editactivitymodalcomponent.json (French) commit0ed4e9134dAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:36 2025 +0100 New translations activitysummarycomponent.json (Portuguese) commitb22b45f185Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:35 2025 +0100 New translations activitysummarycomponent.json (Dutch) commit78a49d35afAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:34 2025 +0100 New translations activitysummarycomponent.json (German) commitd03f260e75Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:33 2025 +0100 New translations activitysummarycomponent.json (Catalan) commit1203787f8aAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:32 2025 +0100 New translations activitysummarycomponent.json (Spanish) commit9a948aea26Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:31 2025 +0100 New translations activitysummarycomponent.json (French) commitcf2a0e8bddAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Wed Jun 25 12:38:30 2025 +0100 New translations editactivitymodalcomponent.json (Dutch) commitd722e79394Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Wed Jun 25 12:28:40 2025 +0100 Fix Docker image tag & bump frontend version [docker] fix image tag on docker-compose.yml.example [frontend] bump version on frontend commite15f4e89c7Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Wed Jun 25 12:18:17 2025 +0100 Bump dependencies commit319b83ae0fAuthor: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Wed Jun 25 12:10:53 2025 +0100 Add indoor cycling support & fix config/logging issues [backend] fixed invalid default value for JAEGER_ENABLED [backend] added support for activity type indoor_cycling [backend] fixed logging spacing and added docs to logger [docker] fixed logging spacing [docs] added support for activity type indoor_cycling [frontend] added support for activity type indoor_cycling [frontend] re added activityMaxHR to activitySummaryComponent.json commitf3e298300fMerge:c9e006a710668d3eAuthor: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Wed Jun 25 11:20:16 2025 +0100 Merge branch 'docker_immutable_feature' into pre-release commit10668d3ed6Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Wed Jun 25 09:26:34 2025 +0100 Update env.js commitb138f81f1eAuthor: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Tue Jun 24 23:14:58 2025 +0100 Update database.py commit5c7c81f123Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Tue Jun 24 23:03:14 2025 +0100 Refactor env var usage and runtime config for frontend/backend Backend now uses core.config for environment variables with sensible defaults, reducing direct os.environ access and improving robustness. Dockerfile and start.sh were updated to remove hardcoded env vars and generate a runtime env.js for frontend configuration. Frontend code now reads ENDURAIN_HOST from window.env instead of Vite env, enabling runtime configuration. Obsolete .env file was removed, and documentation was updated to fix a typo in JAEGER_PORT. commitbb2ae4f548Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:27 2025 +0100 New translations activitymandabovepillscomponent.json (Portuguese) commit64f438000fAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:26 2025 +0100 New translations activitymandabovepillscomponent.json (German) commit4129e66768Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:25 2025 +0100 New translations activitymandabovepillscomponent.json (Catalan) commit7c37b2c375Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:23 2025 +0100 New translations activitymandabovepillscomponent.json (Spanish) commited236a0ee7Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:22 2025 +0100 New translations activitymandabovepillscomponent.json (French) commit825ef122d7Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:21 2025 +0100 New translations activitysummarycomponent.json (Portuguese) commitd04742caeeAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:20 2025 +0100 New translations activitysummarycomponent.json (Dutch) commit187b56221bAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:19 2025 +0100 New translations activitysummarycomponent.json (German) commit83847a2d0cAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:18 2025 +0100 New translations activitysummarycomponent.json (Catalan) commit8a2417360aAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:17 2025 +0100 New translations activitysummarycomponent.json (Spanish) commit7bf9dfdc66Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:16 2025 +0100 New translations activitysummarycomponent.json (French) commit683b1fd260Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 16:34:15 2025 +0100 New translations activitymandabovepillscomponent.json (Dutch) commitc9e006a757Merge:1a5f5cca6cfe4999Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 16:28:37 2025 +0100 Merge branch 'pr/199' into pre-release commit6cfe4999e1Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 16:27:44 2025 +0100 Add HR Zones chart to mobile, move logic to chartUtils [frontend] moved chart functions to chartUtils file [frontend] added HR Zones Bar Chart to mobile view commite475138d11Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 16:15:42 2025 +0100 Refactor HR Zones chart, clean up ActivitySummary [frontend] moved HR Zones graph away from ActivitySummaryComponent [frontend] removed duplicated translation [frontend] reverted changes on ActivitySummaryComponent since chart was removed [frontend] moved chart to ActivityMandAbovePillsComponent [frontend] small adjustments to BarChart commita7d8418d51Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 15:04:05 2025 +0100 Improve formatting, docstrings & fix activityActivityStreams prop [backend] fixed formatting [backend] changed existing docstrings to be more complete [backend] changed imports to be consistent with rest of the code [frontend] fixed formatting [frontend] set new prop on ActivitySummaryComponent to be not required because it is not always necessary (HomeView) [frontend] fixed issue onMounted on ActivitySummaryComponent when new prop is null commit1a5f5cca86Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 13:52:31 2025 +0100 Fix for docs commit1960c3153aMerge:c895ae0d935a5d43Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 13:37:08 2025 +0100 Merge branch 'pr/203' into pre-release commitc895ae0d83Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 12:33:21 2025 +0100 New translations activitybellowmpillscomponent.json (Dutch) commit5338c95879Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 12:33:20 2025 +0100 New translations activitylapscomponent.json (Dutch) commit7d3930bfbaAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 12:33:19 2025 +0100 New translations generalitems.json (Dutch) commit0838b6cf6eAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:11:34 2025 +0100 New translations activitybellowmpillscomponent.json (Dutch) commit9776b6461aAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:11:31 2025 +0100 New translations activitylapscomponent.json (Dutch) commit8b2b6c6fc8Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:10:23 2025 +0100 New translations stravacallbackview.json (Dutch) commit6acf86fb47Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:10:09 2025 +0100 New translations summaryview.json (Dutch) commit79708f1aa9Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:10:07 2025 +0100 New translations settingsuserprofilezonecomponent.json (Dutch) commit5e93661fa0Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:10:06 2025 +0100 New translations settingsintegrationszonecomponent.json (Dutch) commitbfa7093fc4Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:10:04 2025 +0100 New translations editactivitymodalcomponent.json (Dutch) commit27036db437Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:10:03 2025 +0100 New translations navbarcomponent.json (Dutch) commitc4143435d1Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:09:48 2025 +0100 New translations activityview.json (Dutch) commit8b1cc16bc6Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:09:47 2025 +0100 New translations loginview.json (Dutch) commit3cd447368eAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:09:46 2025 +0100 New translations generalitems.json (Dutch) commitbac2b90b88Merge:d0f850dd13afbe86Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 11:06:59 2025 +0100 Merge remote-tracking branch 'origin/l10n_pre-release' into pre-release commitd0f850dd0eMerge:ed4c87d4426500ffAuthor: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 11:04:10 2025 +0100 Merge branch 'pr/208' into pre-release commit13afbe86aeAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 11:00:40 2025 +0100 New translations gearslistcomponent.json (German) commit59ad9170b0Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Mon Jun 23 10:59:50 2025 +0100 New translations gearview.json (Catalan) commit426500ff2eAuthor: C2gl <97646342+C2gl@users.noreply.github.com> Date: Mon Jun 23 11:59:36 2025 +0200 fixing some typos commited4c87d407Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com> Date: Mon Jun 23 10:55:01 2025 +0100 Silence StravaLib token warnings + bump dependencies commit6f24ac158fAuthor: C2gl <97646342+C2gl@users.noreply.github.com> Date: Mon Jun 23 01:10:40 2025 +0200 fully translated commit386e5ae853Author: C2gl <97646342+C2gl@users.noreply.github.com> Date: Mon Jun 23 00:57:42 2025 +0200 done upto strava commit56cb31288dAuthor: C2gl <97646342+C2gl@users.noreply.github.com> Date: Mon Jun 23 00:54:16 2025 +0200 next to translate - gears commit8d3158ef6cAuthor: C2gl <97646342+C2gl@users.noreply.github.com> Date: Mon Jun 23 00:25:31 2025 +0200 initial commit for fork commit935a5d43b5Author: Fredrik Fyksen <fredrik@fyksen.me> Date: Wed Jun 18 15:06:48 2025 +0200 Updated hosting guide commit56a7ff881fAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 21:02:22 2025 +0100 New translations summaryview.json (Dutch) commit9c0acab25aAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 19:35:49 2025 +0100 New translations activityview.json (Dutch) commitbfe8ae1d14Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 19:35:48 2025 +0100 New translations summaryview.json (Dutch) commit0c0a375d1aAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 19:35:47 2025 +0100 New translations activitymandabovepillscomponent.json (Dutch) commit8c4619fc8dAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 19:35:46 2025 +0100 New translations editactivitymodalcomponent.json (Dutch) commit9748f7776fAuthor: Zuhdi <rccheattest2@gmail.com> Date: Tue Jun 17 22:57:52 2025 +0700 feat: change hr zone to bar chart - return transform_activity_streams to get_public_activity_stream_by_type - commitfcad6ea3e5Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:27 2025 +0100 New translations summaryview.json (Dutch) commit515427d89bAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:26 2025 +0100 New translations activitiesview.json (Dutch) commit50a76062f0Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:24 2025 +0100 New translations activitymandabovepillscomponent.json (Dutch) commit95c6b9ef0eAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:23 2025 +0100 New translations searchview.json (Dutch) commit503b943a2eAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:22 2025 +0100 New translations settingsuserprofilezonecomponent.json (Dutch) commitb1884f4fe9Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:21 2025 +0100 New translations settingsintegrationszonecomponent.json (Dutch) commit04c899a3a4Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:20 2025 +0100 New translations gearslistcomponent.json (Dutch) commit5163263b88Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:19 2025 +0100 New translations gearsaddeditgearmodalcomponent.json (Dutch) commitae3943d352Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:18 2025 +0100 New translations editactivitymodalcomponent.json (Dutch) commit01a900b166Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:16 2025 +0100 New translations gearview.json (Dutch) commite1c006fd7eAuthor: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:15 2025 +0100 New translations loginview.json (Dutch) commit1f406e5022Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 14:39:14 2025 +0100 New translations generalitems.json (Dutch) commit9b43447128Author: João Vitória Silva <joao.vitoria.silva@pm.me> Date: Tue Jun 17 12:53:54 2025 +0100 New translations navbarcomponent.json (Dutch) commitc503c35607Author: Zuhdi <rccheattest2@gmail.com> Date: Tue Jun 17 08:01:37 2025 +0700 chore: fix review commit85c8f9410cAuthor: Zuhdi <rccheattest2@gmail.com> Date: Tue Jun 17 07:15:01 2025 +0700 fix: handle when average_heartrate and max_heartrate is null when import from strava commit7e9b3227c4Author: Zuhdi <rccheattest2@gmail.com> Date: Sun Jun 15 11:44:39 2025 +0700 feat: change default value for hrZones commitbd1ec74968Author: Zuhdi <rccheattest2@gmail.com> Date: Sun Jun 15 11:32:40 2025 +0700 feat: show HR based on zone that calculated from user birthdate (if any)
1107 lines
43 KiB
Python
1107 lines
43 KiB
Python
import fitdecode
|
||
import os
|
||
|
||
from fastapi import HTTPException, status
|
||
from datetime import datetime, timedelta
|
||
import time as timelib
|
||
from sqlalchemy.orm import Session
|
||
from timezonefinder import TimezoneFinder
|
||
from zoneinfo import ZoneInfo, available_timezones
|
||
|
||
import activities.activity.utils as activities_utils
|
||
import activities.activity.schema as activities_schema
|
||
|
||
import activities.activity_exercise_titles.schema as activity_exercise_titles_schema
|
||
import activities.activity_exercise_titles.crud as activity_exercise_titles_crud
|
||
|
||
import activities.activity_workout_steps.schema as activity_workout_steps_schema
|
||
|
||
import users.user_default_gear.utils as user_default_gear_utils
|
||
|
||
import garmin.utils as garmin_utils
|
||
|
||
import gears.gear.crud as gears_crud
|
||
|
||
import users.user_privacy_settings.schema as users_privacy_settings_schema
|
||
|
||
import core.logger as core_logger
|
||
|
||
import core.config as core_config
|
||
|
||
|
||
def create_activity_objects(
|
||
sessions_records: dict,
|
||
user_id: int,
|
||
user_privacy_settings: users_privacy_settings_schema.UsersPrivacySettings,
|
||
garmin_activity_id: int = None,
|
||
garminconnect_gear: dict = None,
|
||
db: Session = None,
|
||
) -> list:
|
||
try:
|
||
# Create an instance of TimezoneFinder
|
||
tf = TimezoneFinder()
|
||
timezone = core_config.TZ
|
||
|
||
# Define variables
|
||
gear_id = None
|
||
|
||
if garminconnect_gear:
|
||
user_integrations = garmin_utils.fetch_user_integrations_and_validate_token(
|
||
user_id, db
|
||
)
|
||
|
||
if user_integrations.garminconnect_sync_gear:
|
||
# set the gear id for the activity
|
||
gear = gears_crud.get_gear_by_garminconnect_id_from_user_id(
|
||
garminconnect_gear[0]["uuid"], user_id, db
|
||
)
|
||
|
||
# set the gear id for the activity
|
||
if gear is not None:
|
||
gear_id = gear.id
|
||
|
||
activities = []
|
||
|
||
for session_record in sessions_records:
|
||
# Define default values
|
||
activity_type = 10
|
||
activity_name = "Workout"
|
||
pace = 0
|
||
|
||
if session_record["session"]["activity_type"]:
|
||
# Set the activity type based on the session record
|
||
activity_type = activities_utils.define_activity_type(
|
||
session_record["session"]["activity_type"]
|
||
)
|
||
|
||
# Set the activity name based on the activity type
|
||
activity_name = (
|
||
activities_utils.set_activity_name_based_on_activity_type(
|
||
activity_type
|
||
)
|
||
)
|
||
|
||
location_fields = ["city", "town"]
|
||
for field in location_fields:
|
||
if session_record["session"].get(field):
|
||
activity_name += f" - {session_record['session'][field]}"
|
||
break
|
||
|
||
if gear_id is None:
|
||
gear_id = (
|
||
user_default_gear_utils.get_user_default_gear_by_activity_type(
|
||
user_id, activity_type, db
|
||
)
|
||
)
|
||
|
||
if (
|
||
session_record["activity_name"]
|
||
and session_record["activity_name"] != "Workout"
|
||
):
|
||
activity_name = session_record["activity_name"]
|
||
|
||
# Calculate elevation gain/loss, pace, average speed, and average power
|
||
total_timer_time, pace = calculate_pace(
|
||
session_record["session"]["distance"],
|
||
session_record["session"]["total_timer_time"],
|
||
session_record["session"]["activity_type"],
|
||
session_record["split_summary"],
|
||
)
|
||
|
||
if activity_type != 3 and activity_type != 7:
|
||
if session_record["is_lat_lon_set"]:
|
||
timezone = tf.timezone_at(
|
||
lat=session_record["lat_lon_waypoints"][0]["lat"],
|
||
lng=session_record["lat_lon_waypoints"][0]["lon"],
|
||
)
|
||
else:
|
||
if session_record["time_offset"]:
|
||
timezone = find_timezone_name(
|
||
session_record["time_offset"],
|
||
session_record["session"]["first_waypoint_time"],
|
||
)
|
||
parsed_activity = {
|
||
# Create an Activity object with parsed data
|
||
"activity": activities_schema.Activity(
|
||
user_id=user_id,
|
||
name=activity_name,
|
||
distance=(
|
||
round(session_record["session"]["distance"])
|
||
if session_record["session"]["distance"]
|
||
else 0
|
||
),
|
||
activity_type=activity_type,
|
||
start_time=session_record["session"][
|
||
"first_waypoint_time"
|
||
].strftime("%Y-%m-%dT%H:%M:%S"),
|
||
end_time=session_record["session"]["last_waypoint_time"].strftime(
|
||
"%Y-%m-%dT%H:%M:%S"
|
||
),
|
||
timezone=timezone,
|
||
total_elapsed_time=session_record["session"]["total_elapsed_time"],
|
||
total_timer_time=total_timer_time,
|
||
city=session_record["session"]["city"],
|
||
town=session_record["session"]["town"],
|
||
country=session_record["session"]["country"],
|
||
elevation_gain=session_record["session"]["ele_gain"],
|
||
elevation_loss=session_record["session"]["ele_loss"],
|
||
pace=pace,
|
||
average_speed=session_record["session"]["avg_speed"],
|
||
max_speed=session_record["session"]["max_speed"],
|
||
average_power=session_record["session"]["avg_power"],
|
||
max_power=session_record["session"]["max_power"],
|
||
normalized_power=session_record["session"]["np"],
|
||
average_hr=session_record["session"]["avg_hr"],
|
||
max_hr=session_record["session"]["max_hr"],
|
||
average_cad=session_record["session"]["avg_cadence"],
|
||
max_cad=session_record["session"]["max_cadence"],
|
||
workout_feeling=session_record["session"]["workout_feeling"],
|
||
workout_rpe=session_record["session"]["workout_rpe"],
|
||
calories=session_record["session"]["calories"],
|
||
visibility=(
|
||
user_privacy_settings.default_activity_visibility
|
||
if user_privacy_settings.default_activity_visibility is not None
|
||
else 0
|
||
),
|
||
gear_id=gear_id,
|
||
strava_gear_id=None,
|
||
strava_activity_id=None,
|
||
garminconnect_activity_id=garmin_activity_id,
|
||
garminconnect_gear_id=(
|
||
garminconnect_gear[0]["uuid"] if garminconnect_gear else None
|
||
),
|
||
hide_start_time=user_privacy_settings.hide_activity_start_time
|
||
or False,
|
||
hide_location=user_privacy_settings.hide_activity_location or False,
|
||
hide_map=user_privacy_settings.hide_activity_map or False,
|
||
hide_hr=user_privacy_settings.hide_activity_hr or False,
|
||
hide_power=user_privacy_settings.hide_activity_power or False,
|
||
hide_cadence=user_privacy_settings.hide_activity_cadence or False,
|
||
hide_elevation=user_privacy_settings.hide_activity_elevation
|
||
or False,
|
||
hide_speed=user_privacy_settings.hide_activity_speed or False,
|
||
hide_pace=user_privacy_settings.hide_activity_pace or False,
|
||
hide_laps=user_privacy_settings.hide_activity_laps or False,
|
||
hide_workout_sets_steps=user_privacy_settings.hide_activity_workout_sets_steps
|
||
or False,
|
||
hide_gear=user_privacy_settings.hide_activity_gear or False,
|
||
),
|
||
"is_elevation_set": session_record["is_elevation_set"],
|
||
"ele_waypoints": session_record["ele_waypoints"],
|
||
"is_power_set": session_record["is_power_set"],
|
||
"power_waypoints": session_record["power_waypoints"],
|
||
"is_heart_rate_set": session_record["is_heart_rate_set"],
|
||
"hr_waypoints": session_record["hr_waypoints"],
|
||
"is_velocity_set": session_record["is_velocity_set"],
|
||
"vel_waypoints": session_record["vel_waypoints"],
|
||
"pace_waypoints": session_record["pace_waypoints"],
|
||
"is_cadence_set": session_record["is_cadence_set"],
|
||
"cad_waypoints": session_record["cad_waypoints"],
|
||
"is_lat_lon_set": session_record["is_lat_lon_set"],
|
||
"lat_lon_waypoints": session_record["lat_lon_waypoints"],
|
||
"laps": session_record["laps"],
|
||
"sets": session_record["sets"],
|
||
"workout_steps": session_record["workout_steps"],
|
||
}
|
||
|
||
activities.append(parsed_activity)
|
||
|
||
return activities
|
||
except HTTPException as http_err:
|
||
raise http_err
|
||
except Exception as err:
|
||
# Log the exception
|
||
core_logger.print_to_log(
|
||
f"Error in create_activity_objects: {err}", "error", exc=err
|
||
)
|
||
# Raise an HTTPException with a 500 Internal Server Error status code
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail="Can't parse FIT file sessions",
|
||
) from err
|
||
|
||
|
||
def split_records_by_activity(parsed_data: dict) -> dict:
|
||
sessions = parsed_data["sessions"]
|
||
lat_lon_waypoints = parsed_data["lat_lon_waypoints"]
|
||
ele_waypoints = parsed_data.get("ele_waypoints", [])
|
||
hr_waypoints = parsed_data.get("hr_waypoints", [])
|
||
cad_waypoints = parsed_data.get("cad_waypoints", [])
|
||
power_waypoints = parsed_data.get("power_waypoints", [])
|
||
vel_waypoints = parsed_data.get("vel_waypoints", [])
|
||
pace_waypoints = parsed_data.get("pace_waypoints", [])
|
||
|
||
# Check for each auxiliary flag
|
||
is_lat_lon_set = parsed_data.get("is_lat_lon_set", False)
|
||
is_elevation_set = parsed_data.get("is_elevation_set", False)
|
||
is_heart_rate_set = parsed_data.get("is_heart_rate_set", False)
|
||
is_cadence_set = parsed_data.get("is_cadence_set", False)
|
||
is_power_set = parsed_data.get("is_power_set", False)
|
||
is_velocity_set = parsed_data.get("is_velocity_set", False)
|
||
|
||
# Dictionary to hold split waypoints per activity
|
||
activity_waypoints = {
|
||
i: {
|
||
"lat_lon_waypoints": [] if is_lat_lon_set else None,
|
||
"ele_waypoints": [] if is_elevation_set else None,
|
||
"hr_waypoints": [] if is_heart_rate_set else None,
|
||
"cad_waypoints": [] if is_cadence_set else None,
|
||
"power_waypoints": [] if is_power_set else None,
|
||
"vel_waypoints": [] if is_velocity_set else None,
|
||
"pace_waypoints": [] if is_velocity_set else None,
|
||
}
|
||
for i in range(len(sessions))
|
||
}
|
||
|
||
sessions_records = []
|
||
|
||
# Convert session times to datetime objects for easier comparison
|
||
for i, session in enumerate(sessions):
|
||
# Use the time as is if it’s already a datetime object; otherwise, parse it
|
||
start_time = session["first_waypoint_time"]
|
||
if not isinstance(start_time, datetime):
|
||
start_time = datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S")
|
||
|
||
end_time = session.get("last_waypoint_time", start_time)
|
||
if not isinstance(end_time, datetime):
|
||
end_time = datetime.strptime(end_time, "%Y-%m-%dT%H:%M:%S")
|
||
|
||
# Make both timezone-naive (removes timezone info).
|
||
start_time = start_time.replace(tzinfo=None)
|
||
end_time = end_time.replace(tzinfo=None)
|
||
|
||
laps_records = []
|
||
|
||
if parsed_data["laps"]:
|
||
for lap in parsed_data["laps"]:
|
||
# Check if the lap's start time is within the session's start and end times
|
||
if start_time <= lap["start_time"] <= end_time:
|
||
# Append the lap to the session's laps
|
||
laps_records.append(lap)
|
||
|
||
# Initialize a parsed session dictionary
|
||
parsed_session = {
|
||
"session": session,
|
||
"time_offset": parsed_data["time_offset"],
|
||
"activity_name": parsed_data["activity_name"],
|
||
"lat_lon_waypoints": [],
|
||
"is_lat_lon_set": False,
|
||
"ele_waypoints": [],
|
||
"is_elevation_set": False,
|
||
"hr_waypoints": [],
|
||
"is_heart_rate_set": False,
|
||
"cad_waypoints": [],
|
||
"is_cadence_set": False,
|
||
"power_waypoints": [],
|
||
"is_power_set": False,
|
||
"vel_waypoints": [],
|
||
"pace_waypoints": [],
|
||
"is_velocity_set": False,
|
||
"laps": laps_records,
|
||
"split_summary": parsed_data["split_summary"],
|
||
"workout_steps": parsed_data["workout_steps"],
|
||
"sets": parsed_data["sets"],
|
||
}
|
||
|
||
# Only parse arrays if the respective flag is set
|
||
if is_lat_lon_set:
|
||
activity_waypoints[i]["lat_lon_waypoints"] = [
|
||
wp
|
||
for wp in lat_lon_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["lat_lon_waypoints"]:
|
||
parsed_session["lat_lon_waypoints"] = activity_waypoints[i][
|
||
"lat_lon_waypoints"
|
||
]
|
||
parsed_session["is_lat_lon_set"] = True
|
||
|
||
# If initial latitude and longitude are not set, set them to the first waypoint's coordinates
|
||
if (
|
||
parsed_session["session"]["initial_latitude"] is None
|
||
or parsed_session["session"]["initial_longitude"] is None
|
||
):
|
||
# Set initial latitude and longitude to the first waypoint's coordinates
|
||
parsed_session["session"]["initial_latitude"] = activity_waypoints[
|
||
i
|
||
]["lat_lon_waypoints"][0]["lat"]
|
||
parsed_session["session"]["initial_longitude"] = activity_waypoints[
|
||
i
|
||
]["lat_lon_waypoints"][0]["lon"]
|
||
|
||
# Use geocoding API to get city, town, and country based on coordinates
|
||
location_data = activities_utils.location_based_on_coordinates(
|
||
session["initial_latitude"], session["initial_longitude"]
|
||
)
|
||
|
||
# Extract city, town, and country from location data
|
||
if location_data:
|
||
parsed_session["session"]["city"] = location_data["city"]
|
||
parsed_session["session"]["town"] = location_data["town"]
|
||
parsed_session["session"]["country"] = location_data["country"]
|
||
|
||
if is_elevation_set:
|
||
activity_waypoints[i]["ele_waypoints"] = [
|
||
wp
|
||
for wp in ele_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["ele_waypoints"]:
|
||
parsed_session["ele_waypoints"] = activity_waypoints[i]["ele_waypoints"]
|
||
parsed_session["is_elevation_set"] = True
|
||
if is_heart_rate_set:
|
||
activity_waypoints[i]["hr_waypoints"] = [
|
||
wp
|
||
for wp in hr_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["hr_waypoints"]:
|
||
parsed_session["hr_waypoints"] = activity_waypoints[i]["hr_waypoints"]
|
||
parsed_session["is_heart_rate_set"] = True
|
||
if is_cadence_set:
|
||
activity_waypoints[i]["cad_waypoints"] = [
|
||
wp
|
||
for wp in cad_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["cad_waypoints"]:
|
||
parsed_session["cad_waypoints"] = activity_waypoints[i]["cad_waypoints"]
|
||
parsed_session["is_cadence_set"] = True
|
||
if is_power_set:
|
||
activity_waypoints[i]["power_waypoints"] = [
|
||
wp
|
||
for wp in power_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["power_waypoints"]:
|
||
parsed_session["power_waypoints"] = activity_waypoints[i][
|
||
"power_waypoints"
|
||
]
|
||
parsed_session["is_power_set"] = True
|
||
if is_velocity_set:
|
||
activity_waypoints[i]["vel_waypoints"] = [
|
||
wp
|
||
for wp in vel_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["vel_waypoints"]:
|
||
parsed_session["vel_waypoints"] = activity_waypoints[i]["vel_waypoints"]
|
||
parsed_session["is_velocity_set"] = True
|
||
activity_waypoints[i]["pace_waypoints"] = [
|
||
wp
|
||
for wp in pace_waypoints
|
||
if start_time
|
||
<= datetime.strptime(wp["time"], "%Y-%m-%dT%H:%M:%S")
|
||
<= end_time
|
||
]
|
||
# If there are waypoints, set the parsed session's waypoints and flag
|
||
if activity_waypoints[i]["pace_waypoints"]:
|
||
parsed_session["pace_waypoints"] = activity_waypoints[i][
|
||
"pace_waypoints"
|
||
]
|
||
parsed_session["is_velocity_set"] = True
|
||
|
||
# Append the parsed session to the sessions list
|
||
sessions_records.append(parsed_session)
|
||
|
||
# Return dictionary with each activity's waypoints
|
||
return sessions_records
|
||
|
||
|
||
def parse_fit_file(file: str, db: Session) -> dict:
|
||
try:
|
||
# Initialize default values for various variables
|
||
sessions = []
|
||
time_offset = 0
|
||
last_waypoint_time = None
|
||
activity_name = "Workout"
|
||
|
||
# Arrays to store waypoint data
|
||
lat_lon_waypoints = []
|
||
ele_waypoints = []
|
||
hr_waypoints = []
|
||
cad_waypoints = []
|
||
power_waypoints = []
|
||
vel_waypoints = []
|
||
pace_waypoints = []
|
||
|
||
# Array to store laps
|
||
laps = []
|
||
|
||
# Array to store split waypoints
|
||
splits = []
|
||
|
||
# Array to store split summary info
|
||
split_summary = []
|
||
|
||
# Array to store sets
|
||
sets = []
|
||
|
||
# Array to store workout steps
|
||
workout_steps = []
|
||
|
||
# Array to store exercises titles
|
||
exercises_titles = []
|
||
|
||
# Initialize variables to store previous latitude and longitude
|
||
prev_latitude, prev_longitude = None, None
|
||
|
||
# Initialize variables to store whether elevation, power, heart rate, cadence, and velocity are set
|
||
is_lat_lon_set = False
|
||
is_elevation_set = False
|
||
is_power_set = False
|
||
is_heart_rate_set = False
|
||
is_cadence_set = False
|
||
is_velocity_set = False
|
||
|
||
# Open the FIT file
|
||
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":
|
||
# Initialize session data
|
||
city, town, country = None, None, None
|
||
|
||
# Extract session data
|
||
(
|
||
initial_latitude,
|
||
initial_longitude,
|
||
activity_type,
|
||
first_waypoint_time,
|
||
total_elapsed_time,
|
||
total_timer_time,
|
||
calories,
|
||
distance,
|
||
avg_hr,
|
||
max_hr,
|
||
avg_cadence,
|
||
max_cadence,
|
||
avg_power,
|
||
max_power,
|
||
ele_gain,
|
||
ele_loss,
|
||
np,
|
||
avg_speed,
|
||
max_speed,
|
||
workout_feeling,
|
||
workout_rpe,
|
||
) = parse_frame_session(frame)
|
||
|
||
# If initial latitude and longitude are set, use them to get city, town, and country
|
||
if (
|
||
initial_latitude is not None
|
||
and initial_longitude is not None
|
||
):
|
||
# 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 city, town, and country from location data
|
||
if location_data:
|
||
city = location_data["city"]
|
||
town = location_data["town"]
|
||
country = location_data["country"]
|
||
|
||
# Wait for 1 second (for geocoding API rate limiting)
|
||
timelib.sleep(1)
|
||
|
||
# Initialize the session dictionary with parsed data
|
||
session_data = {
|
||
"initial_latitude": initial_latitude,
|
||
"initial_longitude": initial_longitude,
|
||
"city": city,
|
||
"town": town,
|
||
"country": country,
|
||
"activity_type": activity_type,
|
||
"first_waypoint_time": first_waypoint_time,
|
||
"last_waypoint_time": first_waypoint_time
|
||
+ timedelta(seconds=total_elapsed_time),
|
||
"total_elapsed_time": total_elapsed_time,
|
||
"total_timer_time": total_timer_time,
|
||
"calories": calories,
|
||
"distance": distance,
|
||
"avg_hr": avg_hr,
|
||
"max_hr": max_hr,
|
||
"avg_cadence": avg_cadence,
|
||
"max_cadence": max_cadence,
|
||
"avg_power": avg_power,
|
||
"max_power": max_power,
|
||
"ele_gain": ele_gain,
|
||
"ele_loss": ele_loss,
|
||
"np": np,
|
||
"avg_speed": avg_speed,
|
||
"max_speed": max_speed,
|
||
"workout_feeling": workout_feeling,
|
||
"workout_rpe": workout_rpe,
|
||
}
|
||
|
||
# Append the session data to the sessions list
|
||
sessions.append(session_data)
|
||
|
||
# unknown_147 Sensor Accessories
|
||
|
||
# Extract activity name
|
||
if frame.name == "workout":
|
||
activity_name = parse_frame_workout(frame)
|
||
|
||
# Extract lap data
|
||
if frame.name == "lap":
|
||
laps.append(parse_frame_lap(frame))
|
||
|
||
# Extract split data
|
||
if frame.name in {"split", "unknown_312"}:
|
||
split_data = parse_frame_split(frame)
|
||
split_keys = [
|
||
"split_type",
|
||
"total_elapsed_time",
|
||
"total_timer_time",
|
||
"total_distance",
|
||
"avg_speed",
|
||
"start_time",
|
||
"total_ascent",
|
||
"total_descent",
|
||
"start_position_lat",
|
||
"start_position_long",
|
||
"end_position_lat",
|
||
"end_position_long",
|
||
"max_speed",
|
||
"end_time",
|
||
"total_calories",
|
||
"start_elevation",
|
||
]
|
||
splits.append(dict(zip(split_keys, split_data)))
|
||
|
||
# Extract split summary data
|
||
if frame.name in {"split_summary", "unknown_313"}:
|
||
split_summary_split_type, split_summary_total_timer_time = (
|
||
parse_frame_split_summary(frame)
|
||
)
|
||
split_summary.append(
|
||
{
|
||
"split_type": split_summary_split_type,
|
||
"total_timer_time": split_summary_total_timer_time,
|
||
}
|
||
)
|
||
|
||
# Extract set data
|
||
if frame.name == "set":
|
||
sets.append(parse_frame_set(frame))
|
||
|
||
# Extract workout step data
|
||
if frame.name == "workout_step":
|
||
workout_steps.append(parse_frame_workout_step(frame))
|
||
|
||
# Extract exercise title data
|
||
if frame.name == "exercise_title":
|
||
exercises_titles.append(parse_frame_exercise_title(frame))
|
||
|
||
# Extract waypoint data
|
||
if frame.name == "record":
|
||
# Extract values from record frame
|
||
(
|
||
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 prev_latitude is not None
|
||
and longitude is not None
|
||
and prev_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,
|
||
}
|
||
)
|
||
is_lat_lon_set = True
|
||
|
||
activities_utils.append_if_not_none(
|
||
ele_waypoints, timestamp, elevation, "ele"
|
||
)
|
||
activities_utils.append_if_not_none(
|
||
hr_waypoints, timestamp, heart_rate, "hr"
|
||
)
|
||
activities_utils.append_if_not_none(
|
||
cad_waypoints, timestamp, cadence, "cad"
|
||
)
|
||
activities_utils.append_if_not_none(
|
||
power_waypoints, timestamp, power, "power"
|
||
)
|
||
activities_utils.append_if_not_none(
|
||
vel_waypoints, timestamp, instant_speed, "vel"
|
||
)
|
||
activities_utils.append_if_not_none(
|
||
pace_waypoints, timestamp, instant_pace, "pace"
|
||
)
|
||
|
||
# Update previous latitude, longitude, and last waypoint time
|
||
prev_latitude, prev_longitude, last_waypoint_time = (
|
||
latitude,
|
||
longitude,
|
||
time,
|
||
)
|
||
|
||
if frame.name == "device_settings":
|
||
time_offset = parse_frame_device_settings(frame)
|
||
time_offset = interpret_time_offset(time_offset)
|
||
|
||
# Check if exercises titles is not none
|
||
if exercises_titles:
|
||
activity_exercise_titles_crud.create_activity_exercise_titles(
|
||
exercises_titles, db
|
||
)
|
||
|
||
# Return parsed data as a dictionary
|
||
return {
|
||
"sessions": sessions,
|
||
"time_offset": time_offset,
|
||
"activity_name": activity_name,
|
||
"is_elevation_set": is_elevation_set,
|
||
"ele_waypoints": ele_waypoints,
|
||
"is_power_set": is_power_set,
|
||
"power_waypoints": power_waypoints,
|
||
"is_heart_rate_set": is_heart_rate_set,
|
||
"hr_waypoints": hr_waypoints,
|
||
"is_velocity_set": is_velocity_set,
|
||
"vel_waypoints": vel_waypoints,
|
||
"pace_waypoints": pace_waypoints,
|
||
"is_cadence_set": is_cadence_set,
|
||
"cad_waypoints": cad_waypoints,
|
||
"is_lat_lon_set": is_lat_lon_set,
|
||
"lat_lon_waypoints": lat_lon_waypoints,
|
||
"laps": laps,
|
||
"splits": splits,
|
||
"split_summary": split_summary,
|
||
"sets": sets,
|
||
"workout_steps": workout_steps,
|
||
}
|
||
except HTTPException as http_err:
|
||
raise http_err
|
||
except Exception as err:
|
||
# Log the exception
|
||
core_logger.print_to_log(f"Error in parse_fit_file: {err}", "error", exc=err)
|
||
# Raise an HTTPException with a 500 Internal Server Error status code
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail="Can't parse FIT file",
|
||
) from err
|
||
|
||
|
||
def parse_frame_session(frame):
|
||
# Extracting coordinates
|
||
initial_latitude, initial_longitude = convert_coordinates_to_degrees(
|
||
get_value_from_frame(frame, "start_position_lat"),
|
||
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"
|
||
elif activity_type == "cycling" and sub_sport == "commuting":
|
||
activity_type = "commuting_ride"
|
||
elif activity_type == "cycling" and sub_sport == "indoor_cycling":
|
||
activity_type = "indoor_ride"
|
||
else:
|
||
activity_type = sub_sport
|
||
|
||
# Extracting time values
|
||
start_time = get_value_from_frame(frame, "start_time")
|
||
total_elapsed_time = get_value_from_frame(frame, "total_elapsed_time")
|
||
total_timer_time = get_value_from_frame(frame, "total_timer_time")
|
||
|
||
# Extracting other values
|
||
return (
|
||
initial_latitude,
|
||
initial_longitude,
|
||
activity_type,
|
||
start_time,
|
||
total_elapsed_time,
|
||
total_timer_time,
|
||
get_value_from_frame(frame, "total_calories"),
|
||
get_value_from_frame(frame, "total_distance"),
|
||
get_value_from_frame(frame, "avg_heart_rate"),
|
||
get_value_from_frame(frame, "max_heart_rate"),
|
||
get_value_from_frame(frame, "avg_cadence"),
|
||
get_value_from_frame(frame, "max_cadence"),
|
||
get_value_from_frame(frame, "avg_power"),
|
||
get_value_from_frame(frame, "max_power"),
|
||
get_value_from_frame(frame, "total_ascent"),
|
||
get_value_from_frame(frame, "total_descent"),
|
||
get_value_from_frame(frame, "normalized_power"),
|
||
get_value_from_frame(frame, "enhanced_avg_speed"),
|
||
get_value_from_frame(frame, "enhanced_max_speed"),
|
||
get_value_from_frame(frame, "workout_feeling"),
|
||
get_value_from_frame(frame, "workout_rpe"),
|
||
)
|
||
|
||
|
||
def parse_frame_workout(frame):
|
||
# Return the extracted name
|
||
return get_value_from_frame(frame, "wkt_name", "Workout")
|
||
|
||
|
||
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 = time.replace(tzinfo=None)
|
||
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 parse_frame_lap(frame):
|
||
keys = [
|
||
"start_time",
|
||
"start_position_lat",
|
||
"start_position_long",
|
||
"end_position_lat",
|
||
"end_position_long",
|
||
"total_elapsed_time",
|
||
"total_timer_time",
|
||
"total_distance",
|
||
"total_cycles",
|
||
"total_calories",
|
||
"avg_heart_rate",
|
||
"max_heart_rate",
|
||
"avg_cadence",
|
||
"max_cadence",
|
||
"avg_power",
|
||
"max_power",
|
||
"total_ascent",
|
||
"total_descent",
|
||
"intensity",
|
||
"lap_trigger",
|
||
"sport",
|
||
"sub_sport",
|
||
"normalized_power",
|
||
"total_work",
|
||
"avg_vertical_oscillation",
|
||
"avg_stance_time",
|
||
"avg_fractional_cadence",
|
||
"max_fractional_cadence",
|
||
"enhanced_avg_speed",
|
||
"enhanced_max_speed",
|
||
"enhanced_min_altitude",
|
||
"enhanced_max_altitude",
|
||
"avg_vertical_ratio",
|
||
"avg_step_length",
|
||
]
|
||
|
||
lap_data = tuple(get_value_from_frame(frame, key) for key in keys)
|
||
lap_dict = dict(zip(keys, lap_data))
|
||
|
||
# Ensure start_time and end_time is timezone-naive
|
||
if isinstance(lap_dict["start_time"], datetime):
|
||
lap_dict["start_time"] = lap_dict["start_time"].replace(tzinfo=None)
|
||
|
||
(
|
||
lap_dict["start_position_lat"],
|
||
lap_dict["start_position_long"],
|
||
) = convert_coordinates_to_degrees(
|
||
lap_dict["start_position_lat"],
|
||
lap_dict["start_position_long"],
|
||
)
|
||
lap_dict["end_position_lat"], lap_dict["end_position_long"] = (
|
||
convert_coordinates_to_degrees(
|
||
lap_dict["end_position_lat"],
|
||
lap_dict["end_position_long"],
|
||
)
|
||
)
|
||
|
||
if lap_dict["enhanced_avg_speed"]:
|
||
lap_dict["enhanced_avg_pace"] = 1 / lap_dict["enhanced_avg_speed"]
|
||
|
||
if lap_dict["enhanced_max_speed"]:
|
||
lap_dict["enhanced_max_pace"] = 1 / lap_dict["enhanced_max_speed"]
|
||
|
||
return lap_dict
|
||
|
||
|
||
def parse_frame_split(frame):
|
||
# Define a list of keys and their default values
|
||
keys_defaults = [
|
||
("split_type", 0),
|
||
("total_elapsed_time", 1),
|
||
("total_timer_time", 2),
|
||
("total_distance", 3),
|
||
("avg_speed", 4),
|
||
("start_time", 9),
|
||
("total_ascent", 13),
|
||
("total_descent", 14),
|
||
("start_position_lat", 21),
|
||
("start_position_long", 22),
|
||
("end_position_lat", 23),
|
||
("end_position_long", 24),
|
||
("max_speed", 25),
|
||
("end_time", 27),
|
||
("total_calories", 28),
|
||
("start_elevation", 74),
|
||
]
|
||
|
||
# Extract values using the keys and defaults
|
||
values = [
|
||
get_value_from_frame(frame, key, get_value_from_frame(frame, default))
|
||
for key, default in keys_defaults
|
||
]
|
||
|
||
return tuple(values)
|
||
|
||
|
||
def parse_frame_split_summary(frame):
|
||
# split type
|
||
split_type = get_value_from_frame(frame, "split_type")
|
||
if split_type is None:
|
||
split_type = get_value_from_frame(frame, 0)
|
||
# total working time
|
||
total_timer_time = get_value_from_frame(frame, "total_timer_time")
|
||
if total_timer_time is None:
|
||
total_timer_time = get_value_from_frame(frame, 4)
|
||
if total_timer_time is not None:
|
||
total_timer_time = total_timer_time / 1000
|
||
|
||
return split_type, total_timer_time
|
||
|
||
|
||
def parse_frame_set(frame):
|
||
keys_value = [
|
||
"duration",
|
||
"repetitions",
|
||
"weight",
|
||
"set_type",
|
||
"start_time",
|
||
]
|
||
|
||
keys_raw = [
|
||
"category",
|
||
"category_subtype",
|
||
]
|
||
|
||
set_data = [get_value_from_frame(frame, key) for key in keys_value]
|
||
set_data.extend(get_raw_value_from_frame(frame, key) for key in keys_raw)
|
||
|
||
# Adjust category based on category_subtype
|
||
if set_data[5] is None:
|
||
set_data[5] = 0 if set_data[6] is not None else None
|
||
|
||
return list(set_data)
|
||
|
||
|
||
def parse_frame_workout_step(frame):
|
||
keys_value = [
|
||
"message_index",
|
||
"duration_type",
|
||
"duration_value",
|
||
"target_type",
|
||
"target_value",
|
||
"intensity",
|
||
"notes",
|
||
"exercise_weight",
|
||
"weight_display_unit",
|
||
]
|
||
|
||
keys_raw = [
|
||
"exercise_category",
|
||
"exercise_name",
|
||
]
|
||
|
||
workout_set_data = [get_value_from_frame(frame, key) for key in keys_value]
|
||
workout_set_data.extend(get_raw_value_from_frame(frame, key) for key in keys_raw)
|
||
|
||
secondary_target_value = None
|
||
|
||
if workout_set_data[3] == "swim_stroke":
|
||
if isinstance(workout_set_data[4], str):
|
||
secondary_target_value = workout_set_data[4]
|
||
workout_set_data[4] = None
|
||
elif isinstance(workout_set_data[4], int) and workout_set_data[4] == 255:
|
||
secondary_target_value = "any stroke"
|
||
workout_set_data[4] = None
|
||
|
||
if workout_set_data[5] == 7:
|
||
workout_set_data[5] = "active"
|
||
|
||
if workout_set_data[9] is None:
|
||
workout_set_data[9] = 0 if workout_set_data[10] is not None else None
|
||
|
||
return activity_workout_steps_schema.ActivityWorkoutSteps(
|
||
message_index=workout_set_data[0] if workout_set_data[0] else 0,
|
||
duration_type=workout_set_data[1],
|
||
duration_value=workout_set_data[2],
|
||
target_type=workout_set_data[3],
|
||
target_value=workout_set_data[4] if workout_set_data[4] else None,
|
||
intensity=workout_set_data[5] if type(workout_set_data[5]) == str else None,
|
||
notes=workout_set_data[6],
|
||
exercise_category=workout_set_data[9],
|
||
exercise_name=workout_set_data[10] if workout_set_data[10] else None,
|
||
exercise_weight=workout_set_data[7],
|
||
weight_display_unit=workout_set_data[8],
|
||
secondary_target_value=secondary_target_value,
|
||
)
|
||
|
||
|
||
def parse_frame_exercise_title(frame):
|
||
keys_value = [
|
||
"wkt_step_name",
|
||
]
|
||
|
||
keys_raw = [
|
||
"exercise_category",
|
||
"exercise_name",
|
||
]
|
||
|
||
exercise_title_data = [get_value_from_frame(frame, key) for key in keys_value]
|
||
exercise_title_data.extend(get_raw_value_from_frame(frame, key) for key in keys_raw)
|
||
|
||
return activity_exercise_titles_schema.ActivityExerciseTitles(
|
||
exercise_category=exercise_title_data[1] if exercise_title_data[1] else 0,
|
||
exercise_name=exercise_title_data[2] if exercise_title_data[2] else 0,
|
||
wkt_step_name=str(exercise_title_data[0]),
|
||
)
|
||
|
||
|
||
def parse_frame_device_settings(frame):
|
||
return get_value_from_frame(frame, "time_offset")
|
||
|
||
|
||
def interpret_time_offset(raw_offset):
|
||
# Check for two's complement representation (values > 2^31)
|
||
if raw_offset != 0 and raw_offset is not None:
|
||
if raw_offset > 2**31 - 1:
|
||
return raw_offset - 2**32
|
||
return raw_offset
|
||
|
||
|
||
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 get_raw_value_from_frame(frame, key, default=None):
|
||
try:
|
||
raw_value = frame.get_raw_value(key)
|
||
return raw_value if raw_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})
|
||
|
||
|
||
def calculate_pace(distance, total_timer_time, activity_type, split_summary):
|
||
if distance:
|
||
if activity_type != "lap_swimming":
|
||
return total_timer_time, total_timer_time / distance
|
||
time_active = 0
|
||
for split in split_summary:
|
||
if split["split_type"] != 4:
|
||
time_active += split["total_timer_time"]
|
||
|
||
return time_active, time_active / distance
|
||
return total_timer_time, 0
|
||
|
||
|
||
def find_timezone_name(offset_seconds, reference_date):
|
||
for tz_name in available_timezones():
|
||
tz = ZoneInfo(tz_name)
|
||
if reference_date.utcoffset() is None: # Skip invalid timezones
|
||
continue
|
||
|
||
# Get the UTC offset for the reference date
|
||
utc_offset = reference_date.astimezone(tz).utcoffset()
|
||
|
||
if utc_offset.total_seconds() == offset_seconds:
|
||
return tz_name
|