Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions scripts/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,28 @@ def aggregate():
"distance": 0.0,
"moving_time": 0.0,
"elevation_gain": 0.0,
"hr_weighted_sum": 0.0,
"hr_time": 0.0,
"activity_ids": [],
}
entry["count"] += 1
entry["distance"] += float(item.get("distance", 0.0))
entry["moving_time"] += float(item.get("moving_time", 0.0))
entry["elevation_gain"] += float(item.get("elevation_gain", 0.0))
item_hr = float(item.get("heart_rate", 0.0) or 0.0)
item_time = float(item.get("moving_time", 0.0) or 0.0)
if item_hr > 0.0 and item_time > 0.0:
entry["hr_weighted_sum"] += item_hr * item_time
entry["hr_time"] += item_time
entry["activity_ids"].append(item.get("id"))
data[year][activity_type][date] = entry

for year_data in data.values():
for type_data in year_data.values():
for entry in type_data.values():
entry["activity_ids"] = sorted(entry["activity_ids"])
if entry.get("hr_time", 0.0) > 0.0:
entry["heart_rate"] = entry["hr_weighted_sum"] / entry["hr_time"]

output = {
"generated_at": utc_now().isoformat(),
Expand Down
21 changes: 13 additions & 8 deletions scripts/generate_heatmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
format_distance,
format_duration,
format_elevation,
format_heart_rate,
load_config,
normalize_source,
parse_iso_datetime,
Expand Down Expand Up @@ -113,14 +114,16 @@ def _build_title(date_str: str, entry: Dict, units: Dict[str, str]) -> str:
distance = format_distance(entry.get("distance", 0.0), units["distance"])
duration = format_duration(entry.get("moving_time", 0.0))
elevation = format_elevation(entry.get("elevation_gain", 0.0), units["elevation"])

return (
f"{date_str}\n"
f"{count} workout{'s' if count != 1 else ''}\n"
f"Distance: {distance}\n"
f"Duration: {duration}\n"
f"Elevation: {elevation}"
)
lines = [
f"{date_str}",
f"{count} workout{'s' if count != 1 else ''}",
f"Distance: {distance}",
f"Duration: {duration}",
f"Elevation: {elevation}",
]
if float(entry.get("hr_time", 0.0) or 0.0) > 0.0:
lines.append(f"Heart rate: {format_heart_rate(entry.get('heart_rate', 0.0))}")
return "\n".join(lines)


def _color_scale(accent: str) -> List[str]:
Expand Down Expand Up @@ -374,6 +377,8 @@ def _svg_for_year(
"distance": 0.0,
"moving_time": 0.0,
"elevation_gain": 0.0,
"hr_weighted_sum": 0.0,
"hr_time": 0.0,
"activity_ids": [],
})
count = int(entry.get("count", 0))
Expand Down
13 changes: 13 additions & 0 deletions scripts/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
coalesce as _shared_coalesce,
get_nested as _shared_get_nested,
pick_duration_seconds as _shared_pick_duration_seconds,
pick_heart_rate as _shared_pick_heart_rate,
)
from utils import ensure_dir, load_config, normalize_source, parse_iso_datetime, raw_activity_dir, read_json, write_json

Expand All @@ -28,6 +29,10 @@ def _pick_duration_seconds(*values: Any) -> float:
return _shared_pick_duration_seconds(*values)


def _pick_heart_rate(*values: Any) -> float:
return _shared_pick_heart_rate(*values)


def _duration_candidates(activity: Dict[str, Any]) -> List[Any]:
return [
activity.get("moving_time"),
Expand Down Expand Up @@ -82,6 +87,12 @@ def _normalize_activity(activity: Dict, type_aliases: Dict[str, str], source: st
activity.get("elevationGain"),
activity.get("totalElevationGain"),
)
heart_rate = _pick_heart_rate(
activity.get("average_heartrate"),
activity.get("averageHR"),
_get_nested(activity, ["summaryDTO", "averageHR"]),
_get_nested(activity, ["activitySummary", "averageHR"]),
)
activity_name = str(_coalesce(activity.get("name"), activity.get("activityName"), "") or "").strip()

normalized = {
Expand All @@ -96,6 +107,8 @@ def _normalize_activity(activity: Dict, type_aliases: Dict[str, str], source: st
"moving_time": _safe_float(moving_time),
"elevation_gain": _safe_float(elevation_gain),
}
if heart_rate > 0.0:
normalized["heart_rate"] = heart_rate
if activity_name:
normalized["name"] = activity_name
return normalized
Expand Down
14 changes: 14 additions & 0 deletions scripts/provider_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ def pick_duration_seconds(*values: Any) -> float:
if number > 0:
return number
return first_numeric if first_numeric is not None else 0.0


def pick_heart_rate(*values: Any) -> float:
"""Return the first plausible resting/active HR value (0 < hr <= 255)."""
for value in values:
if value in (None, "", []):
continue
try:
number = float(value)
except (TypeError, ValueError):
continue
if 0.0 < number <= 255.0:
return number
return 0.0
16 changes: 16 additions & 0 deletions scripts/sync_garmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
coalesce as _shared_coalesce,
get_nested as _shared_get_nested,
pick_duration_seconds as _shared_pick_duration_seconds,
pick_heart_rate as _shared_pick_heart_rate,
)
from sync_scope import (
activity_scope_from_config,
Expand Down Expand Up @@ -88,6 +89,10 @@ def _pick_duration_seconds(*values: Any) -> float:
return _shared_pick_duration_seconds(*values)


def _pick_heart_rate(*values: Any) -> float:
return _shared_pick_heart_rate(*values)


def _get_nested(payload: Dict[str, Any], keys: List[str]) -> Any:
return _shared_get_nested(payload, keys)

Expand Down Expand Up @@ -127,6 +132,15 @@ def _normalize_activity(activity: Dict[str, Any]) -> Dict[str, Any]:
activity.get("total_elevation_gain"),
)
distance = _coalesce(activity.get("distance"), activity.get("totalDistance"), 0.0)
heart_rate = _pick_heart_rate(
activity.get("averageHR"),
activity.get("averageHeartRate"),
activity.get("average_heartrate"),
_get_nested(activity, ["summaryDTO", "averageHR"]),
_get_nested(activity, ["summaryDTO", "averageHeartRate"]),
_get_nested(activity, ["activitySummary", "averageHR"]),
_get_nested(activity, ["activitySummary", "averageHeartRate"]),
)
activity_name = str(
_coalesce(
activity.get("activityName"),
Expand Down Expand Up @@ -155,6 +169,8 @@ def _normalize_activity(activity: Dict[str, Any]) -> Dict[str, Any]:
"total_elevation_gain": _safe_float(elevation_gain, 0.0),
"provider": "garmin",
}
if heart_rate > 0.0:
normalized["average_heartrate"] = heart_rate
if activity_name:
normalized["name"] = activity_name
return normalized
Expand Down
4 changes: 4 additions & 0 deletions scripts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,7 @@ def format_elevation(meters: float, unit: str) -> str:
return f"{meters:.0f} m"
feet = meters * 3.28084
return f"{feet:.0f} ft"


def format_heart_rate(bpm: float) -> str:
return f"{int(round(bpm))} bpm"
Loading