Skip to content

Richer Polar sensors + history backfill (full history, not just current data) - #16

Open
MarcinG81 wants to merge 6 commits into
Aohzan:mainfrom
MarcinG81:upstream-full-history
Open

Richer Polar sensors + history backfill (full history, not just current data)#16
MarcinG81 wants to merge 6 commits into
Aohzan:mainfrom
MarcinG81:upstream-full-history

Conversation

@MarcinG81

Copy link
Copy Markdown

Summary

This makes the Polar integration much more useful by exposing more of the data the Polar AccessLink API provides and, crucially, by backfilling history — so you no longer only see "current" values that start accumulating from install time, but the actual recent past as well.

It bundles three things (happy to split into separate PRs if you prefer):

1. Connection test on setup

After submitting the credentials, the config flow now runs a pre-flight check (reachability to Polar + that the Client ID is accepted) and logs the outcome, re-showing the form with a clear cannot_connect / invalid_auth error. Previously a wrong URL, no connectivity or a bad client only surfaced later, mid-OAuth, with an unclear failure.

2. More sensors

Field names/units taken from the official AccessLink OpenAPI spec:

  • Heart rate – latest continuous heart rate sample (min/max/average of the day as attributes)
  • Heart rate variability and Breathing rate – from Nightly Recharge
  • Deep / Light / REM sleep – stage durations (selectable unit, plus a human-readable Xh Ym duration attribute)
  • Cardio load (training load) – with strain, tolerance, cardio_load_ratio, cardio_load_status
  • Last exercise heart rate average / maximum

New AccessLink helpers for continuous heart rate and cardio load; both degrade gracefully on HTTP errors (unsupported device, missing consent, no data for a day) instead of failing the coordinator update.

3. History backfill — the big usability win

Home Assistant sensors only build history forward from first poll and can't backfill past states. Polar, though, keeps the last ~28 days of nightly/daily data and per-day continuous heart rate samples.

On setup (and once a day after) the integration now imports that history as long-term statistics attached to each sensor's own entity_id (async_import_statistics, resolving the entity from the registry by unique_id). The result: every sensor's History page shows the real past, including data from before the integration was installed — instead of a graph that only starts now.

  • sleep stages, sleep score, HRV, breathing rate, cardio load → one point per day for ~28 days
  • continuous heart rate → hourly min/mean/max for the last 7 days

To make this work, the sleep-stage and sleep-score sensors gained state_class=measurement, and recorder was added as a dependency. The import is best-effort and never fails setup.

Only long-term statistics are backfilled; the instantaneous state graph still only moves forward (HA does not allow inserting past raw states).

Bonus: dashboard + docs

  • polar-dashboard.yaml: a ready-to-use Lovelace dashboard for training + sleep tracking — a Today snapshot (Mushroom) plus Sleep and Training history views (built-in statistics-graph).
  • README updated to document the new sensors, the connection test and the history backfill.

Notes

Net effect

You go from "I only see data from now on" to a dashboard with the full recent history of sleep, recovery (HRV/recharge), heart rate and training load — genuinely usable for someone training and tracking sleep.

Run a pre-flight check in the user step, before the OAuth flow: verify
Home Assistant can reach Polar and that the Client ID is accepted, log
the outcome, and re-show the form with a clear cannot_connect /
invalid_auth error instead of failing later during the OAuth exchange.
Expose more of the Polar AccessLink data:
- Heart rate (continuous, latest sample with min/max/average attributes)
- Cardio load (training load) with strain/tolerance/ratio/status
- Heart rate variability and Breathing rate from Nightly Recharge
- Deep/Light/REM sleep durations (minutes, with an Xh Ym attribute)
- Last exercise heart rate average/maximum

New AccessLink helpers for continuous heart rate and cardio load; both
degrade gracefully on HTTP errors instead of failing the update.
Home Assistant sensors only build history forward and cannot backfill
past states. Polar keeps ~28 days of nightly/daily data and per-day
continuous heart rate samples, so on setup (and daily afterwards) import
that history as long-term statistics attached to each sensor's own
entity_id, making the past visible on the entity History page:
- sleep stages, sleep score, HRV, breathing rate, cardio load: one point
  per day for the last ~28 days;
- continuous heart rate: hourly min/mean/max for the last 7 days.

Sleep stages and sleep score gain state_class=measurement so they
generate statistics; adds the recorder dependency. Best-effort import.
@Aohzan

Aohzan commented Jun 11, 2026

Copy link
Copy Markdown
Owner

thank you @MarcinG81 , I'll test it asap

@MarcinG81

Copy link
Copy Markdown
Author

Sure, I've tested on mine HA seems to work fine no errors.
Screenshot 2026-06-11 at 15 00 03
Screenshot 2026-06-11 at 15 00 12

@MarcinG81

Copy link
Copy Markdown
Author

thank you @MarcinG81 , I'll test it asap

On second thought, I'm starting to question the whole "current value" approach here.

Polar devices (Loop, watches, etc.) don't stream in real time — they sync to Polar Flow in batches, only every so often. On top of that the data itself is essentially daily-granular (sleep, Nightly Recharge and cardio load are one value per day; continuous heart rate is historical samples). So polling the API every 30 minutes for a "current" reading is pretty much pointless: nothing changes between syncs, and a single 5-minute sample exposed as a live state is honestly a bit misleading.

So I'm leaning towards reworking this to be history-first rather than "current value":

drop the artificial "current" sensors (at least the live continuous heart rate) and treat the data as what it is — a historical record;
sync the full data on a much longer cadence (e.g. every ~6 hours instead of every 30 minutes);
on first run pull ~28 days of history, then each cycle only append the missing/new days (incremental), which also keeps us well within Polar's rate limits — especially for continuous heart rate, which costs one API call per day.
The end result: far less pointless polling, and a clean historical record (sleep, recovery, heart rate, training load) instead of a "live" value that isn't really live.

Does this direction make sense to you? Happy to refactor along these lines — and I can keep a small set of "latest known value" sensors if you'd still like a snapshot for cards/automations.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the Polar Home Assistant custom integration by exposing additional Polar AccessLink metrics as first-class sensors and introducing a long-term statistics backfill so sensor history is visible for the recent past (not only from install time).

Changes:

  • Adds multiple new sensors (continuous heart rate, cardio load, HRV, breathing rate, sleep stages, and last-exercise HR stats) and enriches existing sensor behavior/attributes.
  • Implements a long-term statistics history backfill job (run at setup and then daily) to import recent Polar history into Home Assistant’s recorder statistics.
  • Improves UX/docs by adding a setup-time connection test, expanding README documentation, and adding a ready-to-use Lovelace dashboard YAML.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Documents new sensors, setup connection test, and history backfill behavior.
polar-dashboard.yaml Adds a sample Lovelace dashboard leveraging statistics-graph for backfilled history.
custom_components/polar/statistics.py New module that fetches Polar history and imports it into long-term statistics.
custom_components/polar/sensor.py Adds new sensor entity descriptions, sleep stage formatting, and richer attributes handling.
custom_components/polar/polaraccesslink/accesslink.py Adds AccessLink helpers for continuous HR and cardio load; flattens exercise HR stats.
custom_components/polar/manifest.json Adds recorder dependency to support statistics backfill.
custom_components/polar/coordinator.py Fetches and exposes cardio load + continuous HR in coordinator data.
custom_components/polar/const.py Introduces new coordinator data keys/constants for cardio load and continuous HR.
custom_components/polar/config_flow.py Adds setup-time pre-flight connection test and form re-render helper.
custom_components/polar/__init__.py Schedules best-effort history import at setup and at a daily interval.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

unique_suffix,
)
continue
async_import_statistics(hass, _metadata(entity_id, unit), statistics)
Comment on lines +3 to +8
Home Assistant sensors only build history forward from the moment they start
polling and cannot backfill past states. Polar however keeps the last ~28 days
of nightly/daily data and per-day continuous heart rate samples. This module
imports that history as external long-term statistics (``polar:*``) so it shows
up in the statistics graphs / Developer Tools, including data from before the
integration was installed.
@MarcinG81

Copy link
Copy Markdown
Author

Thanks for the review. Two points from the bot:

  1. async_import_statistics await — this is a false positive. In Home Assistant async_import_statistics (and async_add_external_statistics) are @callback functions, not coroutines: they synchronously enqueue the import on the recorder thread and return None. Awaiting them would actually raise (None is not awaitable). The async_ prefix here means "safe to call from the event loop", not "coroutine". Confirmed at runtime — the import runs and populates statistics (it even emits the expected mean_type/unit_class deprecation notices on recent cores).

  2. Misleading docstring — good catch, fixed in 4b13e8d. The module imports statistics onto each sensor's own entity_id (via async_import_statistics), not as external polar:* ids, and the docstring now says so.

Comment thread polar-dashboard.yaml
# VIEW 1 – TODAY (Mushroom, live data)
# ==========================================================================
- title: Today
path: today

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
path: today
type: sections
path: today

it would be better to use the new section view, the the integrated badge/tile card that replace mushroom custom card

@MarcinG81

Copy link
Copy Markdown
Author

Good call — done in the latest commit. The dashboard now has zero custom-card dependencies:

  • Today is a type: sections view using the native tile cards and entity badges (Mushroom dropped entirely);
  • Sleep/Training keep the built-in statistics-graph cards.

README updated accordingly (no HACS frontend cards needed).

@Aohzan

Aohzan commented Jun 11, 2026

Copy link
Copy Markdown
Owner

thank you @MarcinG81 , I'll test it asap

On second thought, I'm starting to question the whole "current value" approach here.

Polar devices (Loop, watches, etc.) don't stream in real time — they sync to Polar Flow in batches, only every so often. On top of that the data itself is essentially daily-granular (sleep, Nightly Recharge and cardio load are one value per day; continuous heart rate is historical samples). So polling the API every 30 minutes for a "current" reading is pretty much pointless: nothing changes between syncs, and a single 5-minute sample exposed as a live state is honestly a bit misleading.

So I'm leaning towards reworking this to be history-first rather than "current value":

drop the artificial "current" sensors (at least the live continuous heart rate) and treat the data as what it is — a historical record; sync the full data on a much longer cadence (e.g. every ~6 hours instead of every 30 minutes); on first run pull ~28 days of history, then each cycle only append the missing/new days (incremental), which also keeps us well within Polar's rate limits — especially for continuous heart rate, which costs one API call per day. The end result: far less pointless polling, and a clean historical record (sleep, recovery, heart rate, training load) instead of a "live" value that isn't really live.

Does this direction make sense to you? Happy to refactor along these lines — and I can keep a small set of "latest known value" sensors if you'd still like a snapshot for cards/automations.

Why I put 1h delay between refresh is to be pretty sure to catch all exercices, because we can only fill the last_exercice and cannot store multiple exercice history at once. I use InfluxDB to keep the history.
If you refresh once a day, you'll miss something if you do two exercices or more at a day

@MarcinG81

Copy link
Copy Markdown
Author

thank you @MarcinG81 , I'll test it asap

On second thought, I'm starting to question the whole "current value" approach here.
Polar devices (Loop, watches, etc.) don't stream in real time — they sync to Polar Flow in batches, only every so often. On top of that the data itself is essentially daily-granular (sleep, Nightly Recharge and cardio load are one value per day; continuous heart rate is historical samples). So polling the API every 30 minutes for a "current" reading is pretty much pointless: nothing changes between syncs, and a single 5-minute sample exposed as a live state is honestly a bit misleading.
So I'm leaning towards reworking this to be history-first rather than "current value":
drop the artificial "current" sensors (at least the live continuous heart rate) and treat the data as what it is — a historical record; sync the full data on a much longer cadence (e.g. every ~6 hours instead of every 30 minutes); on first run pull ~28 days of history, then each cycle only append the missing/new days (incremental), which also keeps us well within Polar's rate limits — especially for continuous heart rate, which costs one API call per day. The end result: far less pointless polling, and a clean historical record (sleep, recovery, heart rate, training load) instead of a "live" value that isn't really live.
Does this direction make sense to you? Happy to refactor along these lines — and I can keep a small set of "latest known value" sensors if you'd still like a snapshot for cards/automations.

Why I put 1h delay between refresh is to be pretty sure to catch all exercices, because we can only fill the last_exercice and cannot store multiple exercice history at once. I use InfluxDB to keep the history. If you refresh once a day, you'll miss something if you do two exercices or more at a day

That's a fair point, and it makes sense for the current model — since only last_exercise is exposed (a single value that gets overwritten), polling often is the only way to make each workout appear as a state change that the recorder / InfluxDB can capture before the next one replaces it.

But I'd argue that's actually a reason to treat exercises as history rather than as a "current" sensor. The /exercises endpoint returns a list of recent exercises, not just one — so we can read all of them in a single call, including two or more on the same day, and import them as historical records directly. Done that way the polling cadence stops mattering for not-missing-workouts: each sync pulls the full recent list, so nothing is lost even if you train twice between syncs, or if a sync is hours apart.

So I'd split it by data type:

Sleep / Nightly Recharge / cardio load / continuous heart rate are daily-granular or historical samples — those genuinely don't need frequent polling; the value only changes once a day (or is backfilled anyway).
Exercises are the event-like exception you raised — but because the API returns the list, importing the list (instead of overwriting a single last_exercise and hoping a poll catches it) removes the "missed a workout" problem regardless of cadence.
Your InfluxDB setup works, of course — this is really just about not depending on a tight poll interval to avoid losing transient single-value states. If we import the exercise list as proper history, you get every workout reliably and can relax the polling for everything else.

Does that match how you see it? Happy to prototype the exercise-list import so we can compare.

@Aohzan

Aohzan commented Jun 11, 2026

Copy link
Copy Markdown
Owner

the sensor value is not the only thing matter for last exercise, in attributes, we have many informations

@MarcinG81

Copy link
Copy Markdown
Author

the sensor value is not the only thing matter for last exercise, in attributes, we have many informations

You're absolutely right — for last_exercise the state is almost the least interesting part; it's the attributes (sport, distance, duration, HR avg/max, training load, calories...) that carry the value. That's a fair argument against my "import as statistics" idea for exercises specifically: long-term statistics only store numeric mean/min/max, so they'd throw away all of that per-exercise detail — and since they bucket by the hour, two workouts close together could even collapse into one.

So I'd treat exercises differently from the daily/continuous metrics. Since /exercises returns the full list with all attributes, a natural fit is to represent each workout as its own record instead of a single overwritten sensor — for example a Calendar entity: one event per exercise (start/end from the session, sport as the title, the remaining attributes in the event details). That keeps every attribute, handles two-or-more workouts a day without overwriting, and — because each sync just re-reads the list and upserts — it doesn't depend on a tight poll interval to avoid missing one. Firing an event per new exercise would additionally cover automations / logbook.

So the split would be:

daily / nightly / continuous metrics → long-term statistics (history without frequent polling);
exercises → list-based, attribute-preserving records (calendar / events), which answers both the "multiple per day" and the "attributes matter" points.
That keeps your rich exercise data fully intact while still cutting the pointless polling for everything else. Worth a quick prototype?

@MarcinG81

Copy link
Copy Markdown
Author

Good point — and it actually generalizes beyond exercises: for most of these the value is in the attributes, not the headline number. last_sleep carries continuity, sleep charge, cycles, interruptions and the three group scores; Nightly Recharge carries ANS charge, beat-to-beat, the HRV/breathing sample series; cardio load carries strain / tolerance / status. Long-term statistics only keep a single number per hour, so a "statistics-only" rework would drop all of that — you're right.

So I think the cleanest design is a hybrid rather than one-or-the-other:

  • keep the attribute-rich "latest known value" sensors (what you already have) — that's where all the detail lives;
  • add a long-term statistics backfill on top, so the numeric trends (sleep score, HRV, cardio load, heart rate...) are visible for the recent past, not only from install time;
  • handle exercises as their own list-based records — a Calendar entity / events — so two-or-more workouts a day are all kept with their full attributes, instead of a single overwritten last_exercise.

That keeps every attribute you care about, fixes the "history starts at install" gap, and removes the dependency on a tight poll interval for not-missing-workouts. Best of both worlds. Happy to prototype the exercise calendar + the backfill so you can see it in practice.

@Aohzan

Aohzan commented Jun 11, 2026

Copy link
Copy Markdown
Owner

I don't like to talk to AI 🤔

@MarcinG81

Copy link
Copy Markdown
Author

I don't like to talk to AI 🤔

Fair enough 🙂 Anyway, I tested it on my fork as a calendar entity and it works nicely. Screenshots below. No. need for external InfluxDB.
Screenshot 2026-06-12 at 07 54 19

@Aohzan

Aohzan commented Jun 12, 2026

Copy link
Copy Markdown
Owner

I don't know if you can do things like that in HA directly?
image

@MarcinG81

Copy link
Copy Markdown
Author

I don't know if you can do things like that in HA directly? image

I'm testing and potentially I've created something like that:
Screenshot 2026-06-12 at 19 18 23

@MarcinG81

Copy link
Copy Markdown
Author

btw don't merge this commits there is critical bug.

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