Skip to content

Add date-range FTP retrieval - #404

Merged
cyberjunky merged 4 commits into
cyberjunky:masterfrom
rifusaki:master
Aug 7, 2026
Merged

Add date-range FTP retrieval#404
cyberjunky merged 4 commits into
cyberjunky:masterfrom
rifusaki:master

Conversation

@rifusaki

@rifusaki rifusaki commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

get_cycling_ftp() returns only the latest cycling FTP value, while get_lactate_threshold(latest=False) already calls Garmin's undocumented historical FTP range endpoint inline. That makes FTP history unavailable as a direct public API and duplicates the endpoint construction.

This PR adds a range-capable sibling:

  • get_functional_threshold_power_range(start_date, end_date=None, *, sport="RUNNING", aggregation="daily")
  • accepts ISO date strings or date objects; end_date defaults to today
  • validates date order, supported sport keys, and aggregation values
  • calls /biometric-service/stats/functionalThresholdPower/range/{start}/{end} with Garmin's required sport, aggregation, and aggregationStrategy=LATEST parameters
  • normalizes sport values consistently with the existing sport-key helpers

get_lactate_threshold(latest=False) now reuses this method for its power result, removing the duplicated endpoint construction without changing its returned structure or latest-value behavior.

get_cycling_ftp() remains unchanged for callers that only need the current cycling FTP.

Test plan

  • pdm run test: 215 passed, 18 integration tests deselected
  • New unit tests cover:
    • cycling URL and normalized sport construction
    • date object inputs and non-default aggregation
    • inverted date ranges
    • unsupported sport keys and aggregation values

Verified against a live Garmin account.

Summary by CodeRabbit

  • New Features

    • Added functional threshold power retrieval for a specific date or date range.
    • Supports date and datetime inputs, sport selection, and daily or weekly aggregation.
    • Added validation for date ranges, sport values, and aggregation options.
  • Bug Fixes

    • Improved lactate-threshold range retrieval with consistent functional threshold power handling.
    • Enhanced validation for reversed date ranges and health metric requests.
    • Improved reliability when retrieving maximum metrics and heart-rate variability data over date ranges.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds a functional threshold power range API with date, sport, and aggregation validation. Lactate-threshold retrieval now uses this helper. Unit tests cover URL construction, date inputs, validation, and related range methods.

Changes

Threshold power range APIs

Layer / File(s) Summary
Functional threshold power range API
garminconnect/__init__.py, tests/test_garmin_unit.py
Adds get_functional_threshold_power_range with date normalization, default end dates, sport validation, aggregation validation, and API request tests. Tests also cover related range URLs and date validation.
Lactate-threshold power integration
garminconnect/__init__.py, tests/test_garmin_unit.py
Lactate-threshold range retrieval calls the new helper. Tests verify validation error propagation and that invalid ranges do not trigger API calls.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: cyberjunky, richard-tarbell

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding date-range functional threshold power retrieval.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@garminconnect/__init__.py`:
- Around line 1442-1451: Update the start_date and end_date normalization logic
to handle datetime values before the generic date checks, converting them with
.date().isoformat() so the range URL always receives YYYY-MM-DD values. Preserve
existing date and string validation behavior, including the default for
end_date.
- Around line 1566-1571: In the functional-threshold calculation flow, move the
get_functional_threshold_power_range call before the speed and heart-rate
requests so inverted ranges are rejected by the helper before any Garmin API
call. Add a regression test covering an inverted start/end range and assert
connectapi is not called.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a196483a-bbe5-48c3-93c1-2dbb541cb1a7

📥 Commits

Reviewing files that changed from the base of the PR and between 650681d and 47b306b.

📒 Files selected for processing (2)
  • garminconnect/__init__.py
  • tests/test_garmin_unit.py

Comment thread garminconnect/__init__.py Outdated
Comment thread garminconnect/__init__.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

1564-1569: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Normalize datetime values before delegation.

At Line 1564, start_date and end_date can already be timestamp strings because the earlier isinstance(value, date) branches also match datetime. The helper then receives "YYYY-MM-DDTHH:MM:SS" instead of a date-only value. Handle datetime before date in get_lactate_threshold, as the helper does. Add a regression test with datetime inputs.

Proposed fix
-        if isinstance(start_date, date):
+        if isinstance(start_date, datetime):
+            start_date = start_date.date().isoformat()
+        elif isinstance(start_date, date):
             start_date = start_date.isoformat()
         else:
             start_date = _validate_date_format(start_date, "start_date")
-        if isinstance(end_date, date):
+        if isinstance(end_date, datetime):
+            end_date = end_date.date().isoformat()
+        elif isinstance(end_date, date):
             end_date = end_date.isoformat()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@garminconnect/__init__.py` around lines 1564 - 1569, Update
get_lactate_threshold so datetime inputs are normalized to date-only values
before the existing date handling and delegation to
get_functional_threshold_power_range; check datetime before date because
datetime is a date subclass. Add a regression test covering datetime start_date
and end_date inputs and verifying the helper receives normalized date values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@garminconnect/__init__.py`:
- Around line 1571-1573: Wrap the lactate threshold URL expressions assigned to
speed_url and heart_rate_url across adjacent f-strings so each line complies
with PEP 8 line-length conventions, without changing the resulting URLs.

---

Duplicate comments:
In `@garminconnect/__init__.py`:
- Around line 1564-1569: Update get_lactate_threshold so datetime inputs are
normalized to date-only values before the existing date handling and delegation
to get_functional_threshold_power_range; check datetime before date because
datetime is a date subclass. Add a regression test covering datetime start_date
and end_date inputs and verifying the helper receives normalized date values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 667e47fe-07cf-4d4e-b5b6-07b69daac41a

📥 Commits

Reviewing files that changed from the base of the PR and between 47b306b and e3d0387.

📒 Files selected for processing (2)
  • garminconnect/__init__.py
  • tests/test_garmin_unit.py

Comment thread garminconnect/__init__.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
garminconnect/__init__.py (1)

1564-1569: ⚠️ Potential issue | 🟠 Major

Normalize datetime inputs before delegation.

datetime is a subclass of date. The caller therefore converts a datetime to YYYY-MM-DDTHH:MM:SS before calling get_functional_threshold_power_range. This bypasses the helper's datetime handling and sends a non-date string to its date validator.

Handle datetime before date for both bounds. Add a regression test for start_date and end_date datetime values.

Proposed fix
-        if isinstance(start_date, date):
+        if isinstance(start_date, datetime):
+            start_date = start_date.date().isoformat()
+        elif isinstance(start_date, date):
             start_date = start_date.isoformat()

-        if isinstance(end_date, date):
+        if isinstance(end_date, datetime):
+            end_date = end_date.date().isoformat()
+        elif isinstance(end_date, date):
             end_date = end_date.isoformat()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@garminconnect/__init__.py` around lines 1564 - 1569, Update the caller
surrounding get_functional_threshold_power_range to detect and normalize
datetime values before the broader date handling for both start_date and
end_date, preserving datetime semantics so the delegated helper receives valid
date inputs. Add a regression test covering datetime values for both bounds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@garminconnect/__init__.py`:
- Around line 1564-1569: Update the caller surrounding
get_functional_threshold_power_range to detect and normalize datetime values
before the broader date handling for both start_date and end_date, preserving
datetime semantics so the delegated helper receives valid date inputs. Add a
regression test covering datetime values for both bounds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2bc90593-b8d9-4ee7-bef7-9119a41ec600

📥 Commits

Reviewing files that changed from the base of the PR and between e3d0387 and 56f81bb.

📒 Files selected for processing (1)
  • garminconnect/__init__.py

@cyberjunky
cyberjunky merged commit 44d6f4f into cyberjunky:master Aug 7, 2026
4 checks passed
@cyberjunky

Copy link
Copy Markdown
Owner

Thanks a lot Martina! I will adapt the start and end params to the rest of the code and use the helpers to check date format...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants