feat: add speed and pace formatting utility and include in activity summary table
This commit is contained in:
@@ -8,6 +8,7 @@ from strava_mcp_server.utils import (
|
||||
format_date_human,
|
||||
format_duration_markdown,
|
||||
get_sport_emoji,
|
||||
format_speed_or_pace,
|
||||
)
|
||||
|
||||
|
||||
@@ -72,6 +73,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
"moving_time": f"{a.get('moving_time', 0) / 60:.1f} min",
|
||||
"total_elevation_gain": f"{a.get('total_elevation_gain', 0):.0f} m",
|
||||
"average_heartrate": a.get("average_heartrate"),
|
||||
"average_speed": a.get("average_speed"),
|
||||
"gear_id": a.get("gear_id"),
|
||||
}
|
||||
)
|
||||
@@ -82,8 +84,8 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
)
|
||||
else:
|
||||
markdown_summary = f"### 🚴 Aktivitäten (Seite {page})\n"
|
||||
markdown_summary += "| 📅 Datum | 🏃 Icon | 🏷️ Name | 📐 Distanz | ⏱️ Zeit | 🏔️ Höhenmeter | ❤️ Ø HR |\n"
|
||||
markdown_summary += "|----------|--------|--------|-----------|--------|--------------|--------|\n"
|
||||
markdown_summary += "| 📅 Datum | 🏷️ Name | 📐 Distanz | ⏱️ Zeit | ⚡ Ø Pace/Geschw. | 🏔️ Höhenmeter | ❤️ Ø HR |\n"
|
||||
markdown_summary += "|----------|--------|-----------|--------|------------------|--------------|--------|\n"
|
||||
for a, raw_act in zip(essential_data, activities):
|
||||
hr = (
|
||||
f"{a['average_heartrate']:.0f} bpm"
|
||||
@@ -92,7 +94,10 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
)
|
||||
time_md = format_duration_markdown(raw_act.get("moving_time"))
|
||||
sport_icon = get_sport_emoji(a["sport_type"])
|
||||
markdown_summary += f"| {a['start_date_local']} | {sport_icon} | {a['name']} | {a['distance']} | {time_md} | {a['total_elevation_gain']} | {hr} |\n"
|
||||
speed_or_pace = format_speed_or_pace(
|
||||
raw_act.get("average_speed"), a["sport_type"]
|
||||
)
|
||||
markdown_summary += f"| {a['start_date_local']} | {sport_icon} {a['name']} | {a['distance']} | {time_md} | {speed_or_pace} | {a['total_elevation_gain']} | {hr} |\n"
|
||||
|
||||
return [
|
||||
_user_text(markdown_summary.strip()),
|
||||
|
||||
@@ -161,3 +161,41 @@ def get_sport_emoji(sport_type: Optional[str]) -> str:
|
||||
return "🛹"
|
||||
else:
|
||||
return "🏃"
|
||||
|
||||
|
||||
def format_speed_or_pace(
|
||||
average_speed_mps: Optional[float | int], sport_type: Optional[str]
|
||||
) -> str:
|
||||
"""
|
||||
Formats speed/pace based on sport type.
|
||||
- Runs, walks, hikes -> Pace (min/km, e.g. '5:30/km')
|
||||
- Cycling and other sports -> Speed (km/h, e.g. '25.4 km/h')
|
||||
"""
|
||||
if not average_speed_mps or float(average_speed_mps) <= 0:
|
||||
return "-"
|
||||
|
||||
try:
|
||||
mps = float(average_speed_mps)
|
||||
sport_lower = (sport_type or "").lower()
|
||||
|
||||
# Runs, walks, hikes get pace
|
||||
if "run" in sport_lower or "walk" in sport_lower or "hike" in sport_lower:
|
||||
pace_seconds_per_km = 1000 / mps
|
||||
pace_minutes = int(pace_seconds_per_km // 60)
|
||||
pace_seconds = int(round(pace_seconds_per_km % 60))
|
||||
|
||||
if pace_seconds == 60:
|
||||
pace_minutes += 1
|
||||
pace_seconds = 0
|
||||
|
||||
# Guard against crazy paces (e.g. standing still)
|
||||
if pace_minutes > 59:
|
||||
return "-"
|
||||
|
||||
return f"{pace_minutes}:{pace_seconds:02d}/km"
|
||||
else:
|
||||
# Cycling and other sports get speed in km/h
|
||||
speed_kph = mps * 3.6
|
||||
return f"{speed_kph:.1f} km/h"
|
||||
except Exception:
|
||||
return "-"
|
||||
|
||||
@@ -10,6 +10,7 @@ from strava_mcp_server.utils import (
|
||||
format_duration_table,
|
||||
format_duration_markdown,
|
||||
get_sport_emoji,
|
||||
format_speed_or_pace,
|
||||
)
|
||||
|
||||
|
||||
@@ -223,3 +224,33 @@ class TestGetSportEmoji:
|
||||
|
||||
def test_unknown_returns_default(self):
|
||||
assert get_sport_emoji("SomeOddSport") == "🏃"
|
||||
|
||||
|
||||
class TestFormatSpeedOrPace:
|
||||
def test_invalid_input_returns_hyphen(self):
|
||||
assert format_speed_or_pace(None, "Run") == "-"
|
||||
assert format_speed_or_pace(0, "Run") == "-"
|
||||
assert format_speed_or_pace(-1, "Run") == "-"
|
||||
|
||||
def test_running_pace_exact(self):
|
||||
# 3.0303 m/s = ~5:30 min/km
|
||||
# 1000 / 3.0303 = 330 seconds = 5m 30s
|
||||
assert format_speed_or_pace(3.0303, "Run") == "5:30/km"
|
||||
|
||||
def test_running_pace_rounds_correctly(self):
|
||||
# 3.0211 m/s = 330.99 seconds -> rounds to 331s -> 5:31/km
|
||||
assert format_speed_or_pace(3.0211, "VirtualRun") == "5:31/km"
|
||||
|
||||
def test_cycling_speed(self):
|
||||
# 10 m/s = 36 km/h
|
||||
assert format_speed_or_pace(10, "Ride") == "36.0 km/h"
|
||||
# 7.5 m/s = 27 km/h
|
||||
assert format_speed_or_pace(7.5, "MountainBikeRide") == "27.0 km/h"
|
||||
|
||||
def test_hike_gets_pace(self):
|
||||
# 2 m/s = 500s/km = 8:20/km
|
||||
assert format_speed_or_pace(2, "Hike") == "8:20/km"
|
||||
|
||||
def test_other_sports_get_speed(self):
|
||||
# 10 m/s = 36 km/h
|
||||
assert format_speed_or_pace(10, "Swim") == "36.0 km/h"
|
||||
|
||||
Reference in New Issue
Block a user