Files
2026-06-16 15:14:37 +02:00

5.0 KiB

Garmin Coach to Calendar Sync

This tool clones Garmin Coach/adaptive cycling workouts into normal Garmin Connect cycling workouts and schedules the generated clone on your Garmin calendar. The target use case is an original Garmin Edge 1030: the Edge can sync and run the generated structured workout like a normal workout while still using normal Edge navigation.

It uses unofficial Garmin Connect APIs. Run it locally or on a private network unless you harden it yourself.

Current Status

Implemented:

  • Feasibility research in docs/feasibility.md.
  • CLI probe scripts for Garmin login, discovery, Coach extraction, dummy upload/schedule, and clone dry-run.
  • Dockerized FastAPI web app with first-launch local login setup, Garmin login/MFA, manual sync, scheduled change detection, logs, traces, search, and schedule editing.
  • SQLite state under /data/app.db.
  • Persistent Garmin tokens under /data/garmin-tokens.
  • Coolify-compatible Dockerfile and docker-compose.yml.

Still fragile:

  • Garmin can change private API endpoints or authentication behavior.
  • Garmin Coach completion may not be marked when you complete the generated clone.
  • Edge 1030 acceptance must be verified on your device after upload/schedule.
  • FIT export fallback is not implemented because Garmin Connect scheduling is the primary path.

Quick Start

cp .env.example .env
docker compose up --build

Open http://localhost:8080.

First launch:

  1. Create the local app username/password.
  2. Open Garmin.
  3. Enter Garmin credentials.
  4. Enter MFA if Garmin asks for it.
  5. Use Search to inspect Coach/calendar/workout visibility.
  6. Use Dashboard > Dry-run today first, then Check today or Check tomorrow.

Coolify Deployment

Deploy from the repository link.

Set:

  • Container port: 8000
  • Persistent storage mount: /data
  • APP_SECRET_KEY: a long random secret
  • TZ: Europe/Berlin
  • Optional bootstrap APP_USERNAME / APP_PASSWORD

The compose file uses a named volume for local Docker. In Coolify, configure persistent storage for /data instead of a repo bind mount. Runtime state, Garmin tokens, SQLite, logs, and traces all live under /data.

Environment

Important defaults are in .env.example:

TZ=Europe/Berlin
DATA_DIR=/data
APP_USERNAME=admin
APP_PASSWORD=change-me
APP_SECRET_KEY=replace-with-a-long-random-secret
SYNC_ENABLED=true
SYNC_DAYS_AHEAD=1
CLONE_PREFIX=GCClone
OVERWRITE_EXISTING=true
DELETE_OLD_CLONES=false
CHANGE_DETECTION_INTERVAL_MINUTES=30
CHANGE_DETECTION_ACTIVE_WINDOW=05:00-22:00
CHANGE_DETECTION_FIXED_TIMES=05:15,06:15,07:15,08:15,09:15,10:15,11:15,12:15,13:15

Scheduled runs are change-detection runs. They create a missing clone, skip an unchanged clone, and replace only when the Coach source hash changes. If a Coach day becomes rest/empty while a generated clone exists, scheduled sync records a warning and leaves the clone alone.

Web UI

  • Dashboard: Garmin status, last run, next run, clone state, manual sync actions.
  • Garmin: store encrypted Garmin credentials and complete MFA.
  • Search: list calendar entries, normal workouts, plans, Coach tasks, and generated clones.
  • Schedule: edit sync enabled, interval window, fixed :15 checks, days ahead, or restore defaults.
  • Traces: inspect source Coach task/detail, generated payload, source hash, and upload/schedule result.
  • Logs: sync events and warnings.

Passwords, MFA codes, cookies, headers, and tokens are not logged. Trace JSON is redacted before storage.

CLI Probe Commands

The CLI remains useful for low-level debugging:

uv run python scripts/probe_garmin.py login
uv run python scripts/probe_garmin.py report --dump-json
uv run python scripts/probe_garmin.py coach --date today --dump-json
uv run python scripts/clone_today_workout.py --date today --dry-run --dump-json
uv run python scripts/clone_today_workout.py --date today --schedule

Run local verification:

uv run ruff check .
uv run ty check
uv run pytest

Edge 1030 Testing

After a web or CLI sync creates a clone:

  1. Sync the Edge 1030.
  2. Check Training > Workouts.
  3. Check Training > Training Plan > calendar icon > scheduled date.
  4. Start a route/course.
  5. Start the generated workout and verify prompts run while navigation remains active.

More detail is in docs/edge1030-testing.md.

Cleanup

Generated workouts use the GCClone YYYY-MM-DD ... prefix by default. Scheduled automation never deletes a current clone just because a check ran. Replacement only happens when the source hash changes and the stored mapping points to a generated clone.

Use the CLI guarded cleanup helpers if you need manual cleanup:

uv run python scripts/probe_garmin.py generated --prefix GCClone
uv run python scripts/probe_garmin.py generated-calendar --date today --prefix GCClone

Delete or unschedule only IDs shown by those generated-prefix listings.