Zugang zu meinen Daten von Polar, dem Uhren-hersteller.
  • Python 99.8%
  • Dockerfile 0.2%
Find a file
2026-07-05 21:54:25 +02:00
app Simplify game dashboard layout 2026-07-05 21:54:25 +02:00
data initial commit 2026-06-15 10:51:48 +02:00
docs Use proper German umlauts 2026-07-05 19:26:25 +02:00
tests Use recovery data for game readiness 2026-07-05 21:35:43 +02:00
.dockerignore initial commit 2026-06-15 10:51:48 +02:00
.env.docker.example Add San Pepelone wind window 2026-07-05 17:23:40 +02:00
.env.example Add San Pepelone wind window 2026-07-05 17:23:40 +02:00
.gitignore initial commit 2026-06-15 10:51:48 +02:00
docker-compose.yml changed port numer 2026-06-17 11:26:24 +02:00
Dockerfile initial commit 2026-06-15 10:51:48 +02:00
pyproject.toml initial commit 2026-06-15 10:51:48 +02:00
README.md Ingest extended Polar raw data 2026-07-05 20:41:00 +02:00

Polar Gamification API

Small FastAPI service and website for a single Polar AccessLink user. The public start page is the Polar-Expedition gamification status page. Polar OAuth, Daily Activity, raw data views and local editing tools remain available as supporting/maintenance views.

Polar API Basis

The implementation follows the official Polar AccessLink API v3 documentation:

  • Authorization endpoint: GET https://flow.polar.com/oauth2/authorization
  • Token endpoint: POST https://polarremote.com/v2/oauth2/token
  • Register user: POST https://www.polaraccesslink.com/v3/users
  • Daily activity by date: GET /v3/users/activities/{date}
  • Exercises: GET /v3/exercises
  • Exercise sample type mapping follows the official AccessLink appendix: 0=heart_rate bpm, 1=speed km/h, 2=cadence rpm, 3=altitude m, 4=power W, 5=power_pedaling_index %, 6=power_left_right_balance %, 7=air_pressure hpa, 8=running_cadence spm, 9=temperature C, 10=distance m, 11=rr_interval ms

All Polar HTTP calls are isolated in app/services/polar_client.py, so endpoint paths can be changed later if Polar API versions diverge.

Setup

cd polar-gamification-api
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -e ".[dev]"
Copy-Item .env.example .env

Fill .env with your Polar AccessLink client values:

POLAR_CLIENT_ID=...
POLAR_CLIENT_SECRET=...
POLAR_REDIRECT_URI=http://127.0.0.1:8000/auth/callback
APP_BASE_URL=http://127.0.0.1:8000
DATABASE_URL=sqlite:///./polar_state.sqlite3
TIMEZONE=Europe/Berlin
DAILY_ACTIVITY_TODAY_TTL_MINUTES=20
POLAR_DATA_REFRESH_INTERVAL_MINUTES=60
GAME_WIND_SPOT_NAME=San Pepelone
GAME_WIND_LATITUDE=54.036329
GAME_WIND_LONGITUDE=11.578261
GAME_WIND_THRESHOLD_KNOTS=18
GAME_WIND_FORECAST_HOURS=48
GAME_WIND_CACHE_TTL_MINUTES=60

Start the API:

uvicorn app.main:app --reload

Open the website:

Start-Process http://127.0.0.1:8000/

Docker / lokales Netzwerk

Das Projekt kann als Alpine-basiertes Docker-Image gebaut und per Compose gestartet werden. Die SQLite-Datenbank liegt auf dem Host im lokalen Verzeichnis ./data/polar_state.sqlite3 und wird im Container nach /data/polar_state.sqlite3 gemountet. Damit kannst du data/ einfach in deine normalen Backups aufnehmen.

Forgejo-Check-in und Server-Deployment sind separat dokumentiert: docs/forgejo-deploy.md. Das Gamification-Konzept liegt in docs/gamification-concept.md.

cd polar-gamification-api
Copy-Item .env.docker.example .env

Passe in .env die Polar-Werte und die URL deines Servers im lokalen Netz an:

POLAR_CLIENT_ID=...
POLAR_CLIENT_SECRET=...
APP_BASE_URL=http://192.168.178.10:8000
POLAR_REDIRECT_URI=http://192.168.178.10:8000/auth/callback
TIMEZONE=Europe/Berlin
DAILY_ACTIVITY_TODAY_TTL_MINUTES=20
POLAR_DATA_REFRESH_INTERVAL_MINUTES=60
APP_UID=1000
APP_GID=1000
GAME_WIND_SPOT_NAME=San Pepelone
GAME_WIND_LATITUDE=54.036329
GAME_WIND_LONGITUDE=11.578261
GAME_WIND_THRESHOLD_KNOTS=18
GAME_WIND_FORECAST_HOURS=48
GAME_WIND_CACHE_TTL_MINUTES=60

Wichtig: POLAR_REDIRECT_URI muss exakt in der Polar AccessLink Admin UI als Redirect URL hinterlegt sein. Für einen Server im lokalen Netz also nicht 127.0.0.1, sondern die LAN-IP oder der lokale DNS-Name, den dein Browser erreicht.

Start:

mkdir data
docker compose up -d --build
docker compose logs -f

Auf Linux kannst du deine UID/GID so ermitteln:

id -u
id -g

Falls eine vorhandene Datenbank oder das Verzeichnis data/ schon mit falschen Rechten existiert:

sudo chown -R "$(id -u):$(id -g)" data
chmod -R u+rwX data

Danach im Browser öffnen:

http://192.168.178.10:8000/

Stoppen:

docker compose down

Stoppen inklusive gelöschter lokaler SQLite-Daten:

docker compose down
Remove-Item -Recurse -Force .\data

The flow is:

  1. / shows the Polar-Expedition gamification status page.
  2. /auth/start redirects to Polar Flow for login and consent.
  3. /auth/callback stores the AccessLink token and registers the Polar user if needed.
  4. /dashboard shows today's Daily Activity.
  5. /setup keeps the old login/status start page available.

Endpoints

  • GET / website Polar-Expedition gamification page
  • GET /setup website setup/login status page
  • GET /dashboard website activity page
  • GET /game gamification summary as JSON
  • GET /game/view website gamification page, same content as /
  • GET /history/view website history page
  • GET /data/view website overview for all enabled Polar data
  • GET /local/view website for local synced records and edits
  • GET /health-connect/view website for Android Health Connect bridge status
  • GET /health
  • GET /auth/start
  • GET /auth/callback
  • GET /auth/logout
  • GET /today
  • GET /activity/{date}
  • GET /raw/activity/{date}
  • GET /activity/{date}/samples
  • GET /exercises/{date}
  • GET /exercises/{date}/mapped
  • GET /history?days=365
  • GET /data/all
  • GET /data/all?include_samples=true
  • GET /data/all?refresh=true
  • GET /cache/status
  • POST /cache/clear
  • GET /raw/physical-info
  • POST /sync/exercises
  • GET /local/exercises
  • GET /local/exercises/{exercise_id}
  • GET /local/exercises/{exercise_id}/mapped
  • GET /local/exercises/{exercise_id}/raw
  • PUT /local/exercises/{exercise_id}
  • POST /local/exercises/{exercise_id}/discard
  • POST /sync/physical-info
  • GET /local/physical-info
  • PUT /local/physical-info
  • POST /local/physical-info/discard
  • POST /polar/physical-info returns 501, because Polar AccessLink has no write endpoint for physical info
  • POST /health-connect/import
  • GET /health-connect/data
  • GET /health-connect/pending-writes
  • POST /health-connect/pending-writes/ack
  • GET /raw/activity/today
  • GET /raw/exercises/today

Notes

  • Single-user mode stores one token/user state row in SQLite.
  • Polar responses are cached in SQLite in api_cache; default TTL is 6 hours.
  • Daily Activity for the current date is refreshed in the background with DAILY_ACTIVITY_TODAY_TTL_MINUTES; default is 20 minutes, hard minimum is 15 minutes. Requests for today's Daily Activity also use that TTL as their cache boundary. Past dates keep the default cache behavior.
  • Empty Daily Activity responses from Polar (204/null) are cached only for 15 minutes, so delayed watch/app syncs become visible without waiting for the full default cache TTL.
  • The server also refreshes the main Polar data in the background with POLAR_DATA_REFRESH_INTERVAL_MINUTES; default is 60 minutes, hard minimum is 15 minutes. This refresh updates today's Daily Activity, Exercise caches, local Exercise records, Physical Info cache and local Physical Info.
  • The same background refresh stores additional Vantage V2 raw data in api_cache for later analysis: user profile, today's activity samples, continuous heart rate, cardio load, sleep, available sleep, nightly recharge, SleepWise alertness and SleepWise circadian bedtime. These datasets are not displayed or interpreted yet.
  • Editable local records are stored in SQLite in local_records.
  • Synced Polar exercises are stored as JSON envelopes in local_records.payload_json. Each record contains raw with the full Polar response, including route, and mapped with normalized fields, parsed samples, sample statistics, route points and route bounds.
  • Local edits set dirty=1; syncing from Polar preserves dirty local payloads and stores the new Polar payload as source data.
  • Health Connect requires an Android companion app; see docs/health-connect.md.
  • Gamification is computed from cached Daily Activity and local Exercise records. It exposes the Polar-Expedition state: XP, levels, weather-window streaks, day classes, quests, status effects, repair quests, watcher emotion/message, recent days, best days and achievements without writing back to Polar data.
  • The Polar-Expedition also checks the San Pepelone wind forecast through Open-Meteo. When the configured threshold is reached, default 18 kn in the next 48 hours, the game opens a Windfenster and adds the Expedition steht an quest.
  • For another LLM or agent, use docs/llm-api.md as the integration contract.
  • Add refresh=true to supported endpoints to bypass the cache and update from Polar.
  • .env and SQLite database files are ignored by git.
  • AccessLink access tokens do not expire unless revoked, according to Polar's documentation.
  • /today uses the configured timezone, defaulting to Europe/Berlin.