Skip to content

Architecture

Michal Zaniewicz edited this page Jul 14, 2026 · 2 revisions

How the integration is put together. Useful if you're contributing (see Development & Releasing) or just curious why things are split the way they are.

Suunto watch ──▶ Suunto app / Sports Tracker cloud ──▶ Home Assistant (this integration)

The integration is cloud polling (iot_class: cloud_polling): it reads the Sports Tracker cloud that your watch syncs into. It never talks to the watch directly.


Two backends

Host Data Format / auth
api.sports-tracker.com/apiserver/v1/ workouts, lifetime stats ASKO envelope {error, metadata, payload}; header STTAuthorization: <sessionkey>
247.sports-tracker.com/ sleep, recovery, 24/7 activity gzipped NDJSON (gunzipped by magic-byte detection); v1/{sleep|recovery|activity}/export?since=<ms>

Field-level units and gotchas: Data & Units.


Two coordinators (split by cadence)

Live values change often and are cheap; history is heavy (~90 days, paginated) and changes slowly. Re-fetching everything on the fast cadence would be wasteful and rough on an unofficial API - so there are two DataUpdateCoordinators:

Coordinator Default Min Fetches
Fast (live) 15 min 5 the activity stream → current_hr, daily_steps, daily_energy
Daily (history) 60 min 15 sleep (~60 d) + recovery (~5 d) + workouts (~90 d, paginated) + lifetime stats; computes all derived metrics; imports the long-term statistics

The daily coordinator fetches its sections concurrently (asyncio.gather(..., return_exceptions=True)): one flaky stream leaves that section empty while the others still update; all sections failing → UpdateFailed; an auth error → ConfigEntryAuthFailed (triggers reauth).

entry.runtime_data holds both coordinators. Every sensor description declares a source (fast/daily); setup wires each entity to the right coordinator. All entities (and the calendar) share one device keyed (DOMAIN, entry_id).


Auth pipeline

Login is a form POST to api/.../login2 with fields l (email), p (password), totp, timestamp, salt, signature. The signing pipeline is ported byte-for-byte from tajchert/suuntool and validated against its golden vectors (login-secret hex, sample signature, PBKDF2, HOTP).

Login returns {sessionkey, username, userKey}:

  • username becomes the config-entry/device title - that's why your device is named after your Suunto handle.
  • userKey is the config-entry unique id (de-dupes the same account).
  • sessionkey is the bearer token used for all subsequent requests; it is cached and reused across restarts.

Key material is extracted from a specific Suunto Android app version. On a major app version bump the embedded constants may need refreshing (the same "key rotation" concept as suuntool). The credential model (password used once, token persisted) is described in Configuration and Privacy & Disclaimer.


Modules

File Responsibility
__init__.py entry setup/teardown, runtime_data, platform forwarding
api.py HTTP client, ASKO/NDJSON handling, gzip detection, login
auth.py the login signing pipeline (validated against suuntool)
coordinator.py the two coordinators, field mapping, derived-metric assembly, statistics import
metrics.py pure functions: CTL/ATL/TSB, ACWR, baselines, readiness (unit-tested)
statistics.py bucketing + external-statistics import helpers
sensor.py the 56 sensor descriptions
calendar.py the workouts calendar entity
config_flow.py login + reauth + options (poll intervals)
const.py constants, look-back windows, activityId map

Platform / HA API notes

  • Platforms: ["sensor", "calendar"].
  • Minimum HA: 2024.12.0. Uses entry.runtime_data (2024.5+), DataUpdateCoordinator(config_entry=...) (2024.8+), modern reauth helpers (2024.8+), the OptionsFlow config_entry property (2024.11+), and PEP 695 type aliases (Py 3.12).
  • manifest.json declares after_dependencies: ["recorder"] for the statistics import.

Clone this wiki locally