Skip to content

feat: estimate missing calories from power/HR data - #83

Open
lrybak wants to merge 2 commits into
jat255:mainfrom
lrybak:feat/estimate-missing-kcals
Open

feat: estimate missing calories from power/HR data#83
lrybak wants to merge 2 commits into
jat255:mainfrom
lrybak:feat/estimate-missing-kcals

Conversation

@lrybak

@lrybak lrybak commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Some devices — most notably the Hammerhead Karoo — don't write total_calories to their FIT files. After uploading, Garmin Connect shows the activity with 0 kcal, which also skews daily calorie totals and the nutrition module. This PR adds an opt-in, per-profile feature that estimates the missing value from data already recorded in the file and writes it into the Session and Lap messages.

Full transparency: this change was vibe-coded with Claude Code — but every part of it was reviewed, tested against real FIT files from my own rides, and the uploaded results were verified in Garmin Connect.

How it works

A new calorie_calculator.py module supports two methods, in order of preference:

  1. Power-based (used when at least 50% of records carry valid power data):
    integrates mechanical work from the power samples and converts kJ → kcal
    assuming 22% gross efficiency (1 kJ ≈ 1.086 kcal), the same convention
    used by Hammerhead, Garmin, and Strava. No personal data needed.
  2. HR-based fallback: the Keytel et al. (2005) regression, which needs
    weight, age, and sex (stored in the profile, used for nothing else).

The estimation is defensive by design:

  • existing calorie values are never overwritten — only missing ones are
    filled in,
  • power is integrated trapezoidally (mean of the interval endpoints), which stays accurate for sparse "smart recording" samples,
  • if the power meter drops out mid-ride, the affected samples fall back to the HR method (when weight/age/sex are configured) instead of counting zero work; the log message reports how many samples were filled this way,
  • implausible samples are rejected (power > 2000 W, HR outside 30–230 bpm, FIT "invalid" markers),
  • recording gaps are clamped to 30 s so pauses don't inflate the total,
  • the session total is split across laps (samples falling between laps are attributed to the next lap to start), renormalized so the lap values sum up exactly to the session total,
  • if neither method is applicable, the file is processed as before and a warning is logged.

Writing the value required a small workaround in fit_editor.py: when the source file's definition message doesn't include total_calories, the field is not growable in fit_tool, so the editor flips the flag and resets the cached definition so FitFileBuilder(auto_define=True) regenerates it.

Configuration / UI

  • New Profile fields: recalculate_calories, weight_kg, age, sex (all optional; existing configs load unchanged, no migration needed).
  • New optional step in the profile creation wizard, plus full support in the edit wizard: values are pre-filled, Enter keeps them, and answering "No" to the weight/age/sex question removes them from the profile. Disabling the feature keeps the stored values so re-enabling doesn't ask for them again.
  • The profiles table (--list-profiles / TUI) got a "Kcal est." column showing which profiles have the feature enabled.
  • Weight can be entered in kg or lbs.

Tests

  • Full suite passing (324 tests); coverage for config.py and app.py stays at 100%.
  • New tests/test_calorie_calculator.py (14 tests): power integration math, trapezoidal integration, single-sample and extended power dropouts (with and without HR data), HR fallback, method selection, gap clamping, per-lap split consistency including samples falling between or before laps.
  • tests/test_fit_editor.py: end-to-end test on a real Karoo file (calories get injected and survive a rebuild/re-parse round trip) and a guard test proving existing calories are not touched (Zwift file).
  • tests/test_config.py: new profile fields, legacy config compatibility, wizard flows including clearing anthropometric data, profiles table.

Docs

  • New "Calorie Estimation" section in the Multi-Profile Guide (motivation, both methods, dropout behaviour, config field reference) plus a new wizard step in the flow description and updated example config JSON.
  • Short mention with a link in README.md / docs/index.md.
  • AGENTS.md updated with the new module.

@codecov-commenter

codecov-commenter commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (df45da1) to head (3f64ee8).

Additional details and impacted files
@@            Coverage Diff             @@
##              main       #83    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            6         7     +1     
  Lines         1255      1468   +213     
==========================================
+ Hits          1255      1468   +213     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jat255

jat255 commented Jul 14, 2026

Copy link
Copy Markdown
Owner

This is super cool, thanks for the contribution! Can you make sure the test coverage gets up to 100%, and then I'll take a look when it's ready? Thanks!

@lrybak
lrybak marked this pull request as ready for review July 21, 2026 08:09
@lrybak

lrybak commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Hello @jat255, any chance you will have a look into PR?

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.

3 participants