Files
endurain/backend/app/fit/utils.py
João Vitória Silva a20621f866 Squashed commit of the following:
commit d32e3d8fdc
Merge: d722e793 db959207
Author: 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

commit db959207c1
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:51:52 2025 +0100

    New translations activitymandabovepillscomponent.json (Portuguese)

commit 595256934e
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:50:51 2025 +0100

    New translations editactivitymodalcomponent.json (Portuguese)

commit c659dcd84f
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:50:46 2025 +0100

    New translations activitysummarycomponent.json (Portuguese)

commit 1f8e699753
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:41 2025 +0100

    New translations editactivitymodalcomponent.json (Portuguese)

commit c93884c2c7
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:40 2025 +0100

    New translations editactivitymodalcomponent.json (German)

commit 9a59c21a75
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:39 2025 +0100

    New translations editactivitymodalcomponent.json (Catalan)

commit 72ea5b5467
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:38 2025 +0100

    New translations editactivitymodalcomponent.json (Spanish)

commit 7f309e9e32
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:37 2025 +0100

    New translations editactivitymodalcomponent.json (French)

commit 0ed4e9134d
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:36 2025 +0100

    New translations activitysummarycomponent.json (Portuguese)

commit b22b45f185
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:35 2025 +0100

    New translations activitysummarycomponent.json (Dutch)

commit 78a49d35af
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:34 2025 +0100

    New translations activitysummarycomponent.json (German)

commit d03f260e75
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:33 2025 +0100

    New translations activitysummarycomponent.json (Catalan)

commit 1203787f8a
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:32 2025 +0100

    New translations activitysummarycomponent.json (Spanish)

commit 9a948aea26
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:31 2025 +0100

    New translations activitysummarycomponent.json (French)

commit cf2a0e8bdd
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Wed Jun 25 12:38:30 2025 +0100

    New translations editactivitymodalcomponent.json (Dutch)

commit d722e79394
Author: 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

commit e15f4e89c7
Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>
Date:   Wed Jun 25 12:18:17 2025 +0100

    Bump dependencies

commit 319b83ae0f
Author: 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

commit f3e298300f
Merge: c9e006a7 10668d3e
Author: 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

commit 10668d3ed6
Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>
Date:   Wed Jun 25 09:26:34 2025 +0100

    Update env.js

commit b138f81f1e
Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>
Date:   Tue Jun 24 23:14:58 2025 +0100

    Update database.py

commit 5c7c81f123
Author: 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.

commit bb2ae4f548
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:27 2025 +0100

    New translations activitymandabovepillscomponent.json (Portuguese)

commit 64f438000f
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:26 2025 +0100

    New translations activitymandabovepillscomponent.json (German)

commit 4129e66768
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:25 2025 +0100

    New translations activitymandabovepillscomponent.json (Catalan)

commit 7c37b2c375
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:23 2025 +0100

    New translations activitymandabovepillscomponent.json (Spanish)

commit ed236a0ee7
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:22 2025 +0100

    New translations activitymandabovepillscomponent.json (French)

commit 825ef122d7
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:21 2025 +0100

    New translations activitysummarycomponent.json (Portuguese)

commit d04742caee
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:20 2025 +0100

    New translations activitysummarycomponent.json (Dutch)

commit 187b56221b
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:19 2025 +0100

    New translations activitysummarycomponent.json (German)

commit 83847a2d0c
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:18 2025 +0100

    New translations activitysummarycomponent.json (Catalan)

commit 8a2417360a
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:17 2025 +0100

    New translations activitysummarycomponent.json (Spanish)

commit 7bf9dfdc66
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:16 2025 +0100

    New translations activitysummarycomponent.json (French)

commit 683b1fd260
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 16:34:15 2025 +0100

    New translations activitymandabovepillscomponent.json (Dutch)

commit c9e006a757
Merge: 1a5f5cca 6cfe4999
Author: 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

commit 6cfe4999e1
Author: 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

commit e475138d11
Author: 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

commit a7d8418d51
Author: 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

commit 1a5f5cca86
Author: João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>
Date:   Mon Jun 23 13:52:31 2025 +0100

    Fix for docs

commit 1960c3153a
Merge: c895ae0d 935a5d43
Author: 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

commit c895ae0d83
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 12:33:21 2025 +0100

    New translations activitybellowmpillscomponent.json (Dutch)

commit 5338c95879
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 12:33:20 2025 +0100

    New translations activitylapscomponent.json (Dutch)

commit 7d3930bfba
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 12:33:19 2025 +0100

    New translations generalitems.json (Dutch)

commit 0838b6cf6e
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:11:34 2025 +0100

    New translations activitybellowmpillscomponent.json (Dutch)

commit 9776b6461a
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:11:31 2025 +0100

    New translations activitylapscomponent.json (Dutch)

commit 8b2b6c6fc8
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:10:23 2025 +0100

    New translations stravacallbackview.json (Dutch)

commit 6acf86fb47
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:10:09 2025 +0100

    New translations summaryview.json (Dutch)

commit 79708f1aa9
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:10:07 2025 +0100

    New translations settingsuserprofilezonecomponent.json (Dutch)

commit 5e93661fa0
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:10:06 2025 +0100

    New translations settingsintegrationszonecomponent.json (Dutch)

commit bfa7093fc4
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:10:04 2025 +0100

    New translations editactivitymodalcomponent.json (Dutch)

commit 27036db437
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:10:03 2025 +0100

    New translations navbarcomponent.json (Dutch)

commit c4143435d1
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:09:48 2025 +0100

    New translations activityview.json (Dutch)

commit 8b1cc16bc6
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:09:47 2025 +0100

    New translations loginview.json (Dutch)

commit 3cd447368e
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:09:46 2025 +0100

    New translations generalitems.json (Dutch)

commit bac2b90b88
Merge: d0f850dd 13afbe86
Author: 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

commit d0f850dd0e
Merge: ed4c87d4 426500ff
Author: 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

commit 13afbe86ae
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 11:00:40 2025 +0100

    New translations gearslistcomponent.json (German)

commit 59ad9170b0
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Mon Jun 23 10:59:50 2025 +0100

    New translations gearview.json (Catalan)

commit 426500ff2e
Author: C2gl <97646342+C2gl@users.noreply.github.com>
Date:   Mon Jun 23 11:59:36 2025 +0200

    fixing some typos

commit ed4c87d407
Author: 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

commit 6f24ac158f
Author: C2gl <97646342+C2gl@users.noreply.github.com>
Date:   Mon Jun 23 01:10:40 2025 +0200

    fully translated

commit 386e5ae853
Author: C2gl <97646342+C2gl@users.noreply.github.com>
Date:   Mon Jun 23 00:57:42 2025 +0200

    done upto strava

commit 56cb31288d
Author: C2gl <97646342+C2gl@users.noreply.github.com>
Date:   Mon Jun 23 00:54:16 2025 +0200

    next to translate - gears

commit 8d3158ef6c
Author: C2gl <97646342+C2gl@users.noreply.github.com>
Date:   Mon Jun 23 00:25:31 2025 +0200

    initial commit for fork

commit 935a5d43b5
Author: Fredrik Fyksen <fredrik@fyksen.me>
Date:   Wed Jun 18 15:06:48 2025 +0200

    Updated hosting guide

commit 56a7ff881f
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 21:02:22 2025 +0100

    New translations summaryview.json (Dutch)

commit 9c0acab25a
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 19:35:49 2025 +0100

    New translations activityview.json (Dutch)

commit bfe8ae1d14
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 19:35:48 2025 +0100

    New translations summaryview.json (Dutch)

commit 0c0a375d1a
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 19:35:47 2025 +0100

    New translations activitymandabovepillscomponent.json (Dutch)

commit 8c4619fc8d
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 19:35:46 2025 +0100

    New translations editactivitymodalcomponent.json (Dutch)

commit 9748f7776f
Author: 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
    -

commit fcad6ea3e5
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:27 2025 +0100

    New translations summaryview.json (Dutch)

commit 515427d89b
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:26 2025 +0100

    New translations activitiesview.json (Dutch)

commit 50a76062f0
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:24 2025 +0100

    New translations activitymandabovepillscomponent.json (Dutch)

commit 95c6b9ef0e
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:23 2025 +0100

    New translations searchview.json (Dutch)

commit 503b943a2e
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:22 2025 +0100

    New translations settingsuserprofilezonecomponent.json (Dutch)

commit b1884f4fe9
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:21 2025 +0100

    New translations settingsintegrationszonecomponent.json (Dutch)

commit 04c899a3a4
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:20 2025 +0100

    New translations gearslistcomponent.json (Dutch)

commit 5163263b88
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:19 2025 +0100

    New translations gearsaddeditgearmodalcomponent.json (Dutch)

commit ae3943d352
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:18 2025 +0100

    New translations editactivitymodalcomponent.json (Dutch)

commit 01a900b166
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:16 2025 +0100

    New translations gearview.json (Dutch)

commit e1c006fd7e
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:15 2025 +0100

    New translations loginview.json (Dutch)

commit 1f406e5022
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 14:39:14 2025 +0100

    New translations generalitems.json (Dutch)

commit 9b43447128
Author: João Vitória Silva <joao.vitoria.silva@pm.me>
Date:   Tue Jun 17 12:53:54 2025 +0100

    New translations navbarcomponent.json (Dutch)

commit c503c35607
Author: Zuhdi <rccheattest2@gmail.com>
Date:   Tue Jun 17 08:01:37 2025 +0700

    chore: fix review

commit 85c8f9410c
Author: 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

commit 7e9b3227c4
Author: Zuhdi <rccheattest2@gmail.com>
Date:   Sun Jun 15 11:44:39 2025 +0700

    feat: change default value for hrZones

commit bd1ec74968
Author: 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)
2025-06-25 14:22:06 +01:00

1107 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 its 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