Skip to content

Commit 135503f

Browse files
committed
Fix rerun activity weather and HR context
1 parent 3d72d8d commit 135503f

6 files changed

Lines changed: 110 additions & 2 deletions

File tree

chronicle/activity_pipeline.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,6 +2272,14 @@ def _get_garmin_metrics(client: Any | None) -> dict[str, Any]:
22722272
def _merge_hr_cadence_from_strava(training: dict[str, Any], detailed_activity: dict[str, Any]) -> tuple[Any, Any]:
22732273
average_hr = training.get("average_hr", "N/A")
22742274
running_cadence = training.get("running_cadence", "N/A")
2275+
garmin_last = training.get("garmin_last_activity") if isinstance(training.get("garmin_last_activity"), dict) else {}
2276+
if training.get("_garmin_activity_aligned"):
2277+
garmin_average_hr = garmin_last.get("average_hr")
2278+
if garmin_average_hr not in {None, "", "N/A"}:
2279+
average_hr = garmin_average_hr
2280+
garmin_cadence = garmin_last.get("cadence_spm")
2281+
if garmin_cadence not in {None, "", "N/A"}:
2282+
running_cadence = garmin_cadence
22752283

22762284
strava_hr = detailed_activity.get("average_heartrate")
22772285
if average_hr == "N/A" and isinstance(strava_hr, (int, float)):

chronicle/stat_modules/garmin_metrics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,9 @@ def _build_garmin_last_activity_context(
613613
"max_hr": int(round(_as_float(last_activity.get("maxHR")) or 0))
614614
if isinstance(last_activity.get("maxHR"), (int, float))
615615
else "N/A",
616+
"cadence_spm": int(round(_as_float(last_activity.get("averageRunningCadenceInStepsPerMinute")) or 0))
617+
if isinstance(last_activity.get("averageRunningCadenceInStepsPerMinute"), (int, float))
618+
else "N/A",
616619
"avg_power_w": int(round(avg_power)) if avg_power is not None else "N/A",
617620
"norm_power_w": int(round(norm_power)) if norm_power is not None else "N/A",
618621
"max_power_w": int(round(max_power)) if max_power is not None else "N/A",

chronicle/stat_modules/misery_index.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,12 +752,25 @@ def get_misery_index_details_for_activity(
752752
return None
753753

754754
lat, lon = start_latlng
755+
weather_data: dict[str, Any] | None = None
756+
aqi: int | None = None
757+
weather_error: requests.RequestException | None = None
758+
aqi_error: requests.RequestException | None = None
755759
try:
756760
weather_data = _get_weather_data(weather_api_key, float(lat), float(lon), activity_time)
761+
except requests.RequestException as exc:
762+
weather_error = exc
763+
logger.error("Weather API weather request failed: %s", exc)
764+
try:
757765
aqi = _get_air_quality_index(weather_api_key, float(lat), float(lon))
758766
except requests.RequestException as exc:
759-
logger.error("Weather API request failed: %s", exc)
760-
return None
767+
aqi_error = exc
768+
logger.error("Weather API AQI request failed: %s", exc)
769+
770+
if weather_data is None and aqi is None and (weather_error is not None or aqi_error is not None):
771+
if weather_error is not None:
772+
raise weather_error
773+
raise aqi_error if aqi_error is not None else requests.RequestException("Weather API request failed")
761774

762775
if not weather_data:
763776
return {

tests/test_activity_pipeline.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,48 @@ def test_description_context_exposes_activity_badges(self) -> None:
161161
self.assertEqual(context.get("smashrun_activity_badges"), ["Two by 365 by 10k"])
162162
self.assertEqual(context.get("smashrun", {}).get("activity_badges"), ["Two by 365 by 10k"])
163163

164+
def test_description_context_uses_aligned_garmin_activity_hr_and_cadence(self) -> None:
165+
context = _build_description_context(
166+
detailed_activity={
167+
"id": 17455368360,
168+
"name": "Lunch Run",
169+
"type": "Run",
170+
"sport_type": "Run",
171+
"distance": 8046.72,
172+
"moving_time": 2400,
173+
"elapsed_time": 2460,
174+
"average_speed": 3.3528,
175+
"average_heartrate": 151,
176+
"average_cadence": 88.0,
177+
"start_latlng": [33.75, -84.39],
178+
},
179+
training={
180+
"average_hr": 143,
181+
"running_cadence": 176,
182+
"_garmin_activity_aligned": True,
183+
"garmin_last_activity": {
184+
"activity_id": 3002,
185+
"average_hr": 167,
186+
"cadence_spm": 182,
187+
},
188+
"garmin_segment_notables": [],
189+
},
190+
intervals_payload={},
191+
week={"gap": "8:00/mi", "distance": 10.0, "elevation": 100.0, "duration": "1:20:00", "beers_earned": 6.0, "calories": 1000, "run_count": 2},
192+
month={"gap": "8:10/mi", "distance": 40.0, "elevation": 400.0, "duration": "5:20:00", "beers_earned": 25.0, "calories": 4000, "run_count": 8},
193+
year={"gap": "8:20/mi", "distance": 80.0, "elevation": 800.0, "duration": "10:40:00", "beers_earned": 50.0, "calories": 8000, "run_count": 16},
194+
longest_streak=None,
195+
notables=[],
196+
latest_elevation_feet=None,
197+
misery_index=None,
198+
misery_index_description=None,
199+
air_quality_index=None,
200+
aqi_description=None,
201+
)
202+
203+
self.assertEqual(context.get("activity", {}).get("average_hr"), 167)
204+
self.assertEqual(context.get("activity", {}).get("cadence_spm"), 182)
205+
164206

165207
class TestActivitySmashrunBadges(unittest.TestCase):
166208
def test_extract_activity_smashrun_badges_matches_smashrun_and_strava_ids(self) -> None:

tests/test_garmin_metrics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def test_fetch_training_status_returns_expanded_fields(self) -> None:
300300
self.assertEqual(last_activity["distance_miles"], "8.02 mi")
301301
self.assertEqual(last_activity["activity_id"], 21922402831)
302302
self.assertEqual(last_activity["average_pace"], "7:19/mi")
303+
self.assertEqual(last_activity["cadence_spm"], 176)
303304
self.assertIn("Z1", last_activity["hr_zone_summary"])
304305
self.assertEqual(last_activity["total_sets"], 2)
305306
self.assertEqual(last_activity["active_sets"], 2)

tests/test_misery_index.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import unittest
2+
from unittest.mock import patch
3+
4+
import requests
25

36
from chronicle.stat_modules.misery_index import (
47
calculate_misery_index,
58
calculate_misery_index_components,
69
get_aqi_description,
10+
get_misery_index_details_for_activity,
711
get_misery_index_description,
812
)
913

@@ -240,6 +244,43 @@ def test_aqi_description(self) -> None:
240244
self.assertEqual(get_aqi_description(1), "😃")
241245
self.assertEqual(get_aqi_description(99), "Unknown")
242246

247+
def test_activity_details_keep_weather_when_aqi_lookup_fails(self) -> None:
248+
activity = {
249+
"start_date": "2026-03-07T12:00:00Z",
250+
"start_latlng": [33.75, -84.39],
251+
}
252+
with patch(
253+
"chronicle.stat_modules.misery_index._get_weather_data",
254+
return_value={
255+
"temp_f": 62.0,
256+
"dewpoint_f": 48.0,
257+
"humidity": 55.0,
258+
"wind_mph": 4.0,
259+
"cloud": 20.0,
260+
"precip_in": 0.0,
261+
"is_day": True,
262+
"chance_of_rain": 5.0,
263+
"chance_of_snow": 0.0,
264+
"will_it_rain": False,
265+
"will_it_snow": False,
266+
"condition_text": "Sunny",
267+
"heatindex_f": 62.0,
268+
"windchill_f": 62.0,
269+
"tz_id": "America/New_York",
270+
},
271+
), patch(
272+
"chronicle.stat_modules.misery_index._get_air_quality_index",
273+
side_effect=requests.RequestException("aqi unavailable"),
274+
):
275+
details = get_misery_index_details_for_activity(activity, "weather-key")
276+
277+
self.assertIsNotNone(details)
278+
assert details is not None
279+
self.assertIsInstance(details["misery_index"], float)
280+
self.assertEqual(details["aqi"], None)
281+
self.assertEqual(details["aqi_description"], "Unknown")
282+
self.assertEqual(details["weather"]["temp_f"], 62.0)
283+
243284

244285
if __name__ == "__main__":
245286
unittest.main()

0 commit comments

Comments
 (0)