-
Notifications
You must be signed in to change notification settings - Fork 0
Data & Units
Reference for the backend fields and - importantly - their units, which are non-obvious and have bitten this project before. These are confirmed against a live account. If you build templates or debug values, trust this page.
Two Sports Tracker hosts (the same backend the Suunto app uses):
| Host | Carries | Notes |
|---|---|---|
api.sports-tracker.com/apiserver/v1/ |
Workouts, lifetime stats | ASKO envelope {error, metadata, payload}. Auth header STTAuthorization: <sessionkey>. |
247.sports-tracker.com/ |
24/7 wellness streams (sleep, recovery, activity) | gzipped NDJSON, often without a Content-Encoding header (detected by the 1f 8b magic and gunzipped). Paths: v1/{sleep|recovery|activity|sleepstages}/export?since=<ms>. |
See Architecture for how these feed the two coordinators.
| Domain | Field | Stored as | Convert |
|---|---|---|---|
| Sleep |
hrAvg, hrMin
|
Hz | x60 → bpm |
quality, maxSpo2
|
0..1 | x100 → % | |
avgHrv |
ms | - | |
duration, deepSleepDuration, ... |
seconds | /60 → min, /3600 → h | |
| Recovery | balance |
0..1 | x100 → % |
stressState |
enum int | not a fraction | |
| Activity (24/7) | hr |
Hz | x60 → bpm |
energyConsumption |
joules | /4186.8 → kcal. Active energy (above rest), so idle intervals are exactly 0. | |
stepCount |
steps | sum today | |
| Workouts |
avgSpeed, maxSpeed
|
m/s | x3.6 → km/h |
avgPace |
already min/km | - | |
hrdata.workoutAvgHR/MaxHR/userMaxHR |
already bpm | no Hz here! | |
timeInZone0..5 |
centiseconds | /6000 → min | |
totalDistance, totalAscent, totalDescent
|
m | ||
minAltitude, maxAltitude
|
decimetres | /10 → m. Unlike ascent/descent, which really are metres. 0/0 means no barometer reading (indoor). |
|
extensions[].SummaryExtension.pte |
1..5 | Peak Training Effect, Suunto's own session-impact rating. | |
extensions[].SummaryExtension.ascentTime/descentTime
|
s | /60 → min | |
totalTime |
s | ||
energyConsumption |
kcal (per workout) | - | |
recoveryTime |
s | /3600 → h | |
| Lifetime stats | totalDistanceSum |
m | |
totalTimeSum |
s | ||
totalEnergyConsumptionSum |
kcal |
⚠️ Three traps, all found the hard way:
- HR is represented two ways: 24/7 and sleep HR are in Hz (x60), workout HR is already bpm.
- Energy is joules in the 24/7 activity stream (/4186.8 → kcal) but kcal per workout. Reading the stream as calories inflated every figure by 4.19x (fixed in 1.0.14).
- Altitude is decimetres (
minAltitude/maxAltitude, /10) while ascent and descent are metres. Reading it as metres turned an alpine hike into "26788 m" (fixed in 1.0.14).Zone times are centiseconds (/6000 → min, and they sum to roughly
totalTime).
A single night is returned as multiple fragments - one per wake-up. The Suunto app shows their sum. The integration reproduces that:
- Fragments are grouped by a noon-to-noon key:
(local_timestamp - 12h).date(). This keeps a night that crosses midnight together and splits consecutive nights correctly. - Durations are summed; rates (HR, HRV, SpO₂) are duration-weighted across the fragments.
- Wake-up time is derived as the end of the last fragment (the API has no explicit wake field).
Some fields (light/REM sleep, SpO₂, HRV) are absent on some watch models - the corresponding sensors then read unknown.
| Stream | Cadence |
|---|---|
24/7 activity (HR / steps / energy) |
every ~10 min |
recovery (balance / stress) |
every ~30 min |
Workout HR (workouts/{key}/data → heartrates) |
~25 s ({t, hr, d}) |
Workout FIT export (workout/exportFit/{key}) |
~1 s (binary; not used - no FIT dependency) |
sleepstages |
empty on some watches |
These cadences are why the Long-term Statistics are bucketed hourly - that's the granularity HA supports for backdated ingestion, and it's plenty for these sources.
The Sports Tracker workouts list is occasionally eventually-consistent: a whole workout can vanish from one response and reappear the next cycle, which would wobble every workout-derived sensor (counts, weekly volume, CTL/ATL/TSB, statistics). The integration keeps a per-key cache with a 24-hour grace window and re-adds a transiently-missing workout, so a single flaky fetch can't drop it. A genuinely deleted workout falls out after the grace window. (Trade-off: a real deletion/edit takes up to ~24 h to reflect.)
The lifetime-stats payload carries a top-level total plus an allStats[] list with one entry per sport. Its keys differ from the top-level *Sum fields (confirmed live):
| Field | Meaning |
|---|---|
_id |
the activityId (see Activity Types) |
totalDistance |
metres |
totalTime |
seconds |
energyConsumption |
kcal |
numberOfWorkouts |
count |
maxDepth |
metres (diving) |
This feeds the Lifetime by activity sensor: state = number of sports, per-sport totals in the activities attribute.
Each workout carries stopTime (epoch ms) and recoveryTime (seconds). The Recovered-at sensor is derived as stopTime + recoveryTime - the moment Suunto's recovery countdown finishes.
ha-suunto - Unofficial Suunto integration for Home Assistant - Not affiliated with Suunto / Amer Sports / Sports-Tracker - Report an issue
Getting started
Reference
Build with it
Under the hood
Help