Skip to content

COROS Connector

Dreeve imports anything dropped into its watch/ folder. Getting your COROS activities into that folder is what the COROS connector does: it periodically lists new activities, downloads the original .fit files and drops them in. No manual exports.

COROS → connector → watch/ → Dreeve imports it

The connector is a separate container from its own repository, dreeveapp/dreeve-coros-connector. It is not part of Dreeve itself, it only writes files into the folder you already mount. One container serves one COROS account.

Add this alongside the app and daemon services in your docker-compose.yml:

docker-compose.yml
coros-connector:
image: ghcr.io/dreeveapp/dreeve-coros-connector:latest
container_name: dreeve-coros-connector
user: "1000:1000"
read_only: true
restart: unless-stopped
volumes:
- ./watch:/watch
- ./coros/state:/state
- ./coros/tokens:/tokens
networks:
- dreeve-network

Images are published for linux/amd64 and linux/arm64, tagged X.Y.Z, X.Y (latest patch) and latest (highest stable version). Each release shows how to verify its signed build provenance.

user: must be able to write to the three folders. Use your own uid and gid (id -u, id -g), or Dreeve’s PUID and PGID if you set them. Create the folders and give them to that user before the first start, since Docker would otherwise create them owned by root:

Terminal window
> mkdir -p watch coros/state coros/tokens
> sudo chown 1000:1000 watch coros/state coros/tokens
Terminal window
> docker compose run --rm -it coros-connector login

This prompts for your COROS email and password, and is the only command that ever uses them. The password is never logged or saved; only the resulting token is stored in ./coros/tokens.

To log in from a script instead, see Logging in without a terminal.

Terminal window
> docker compose up -d coros-connector
> docker compose logs -f coros-connector

A sync runs immediately, then one every hour. Files land in the watch folder as coros_<labelId>.fit, and the daemon container will import them, exactly as if you had dropped them there yourself.

The first sync reaches back as far as SINCE (30 days by default). To stay clear of COROS’s rate limits, a sync downloads at most MAX_DOWNLOADS_PER_CYCLE activities (25 by default) and picks up where it left off next time. At the default hourly interval that is around 600 activities a day, so a long backfill takes days on purpose.

Each sync also lists the last LOOKBACK_DAYS again, catching activities that reach COROS late.

Every sport is synced by default, triathlon and multi-sport included. Two variables narrow that down, each a comma-separated list of sport names from the table below (case-insensitive) or numeric COROS sportType codes:

  • ACTIVITY_TYPES syncs only the sports it lists.
  • EXCLUDE_ACTIVITY_TYPES never syncs the sports it lists.

An activity is synced when ACTIVITY_TYPES is unset or lists it, and EXCLUDE_ACTIVITY_TYPES does not. Listing the same sport in both, by name or by code, stops the connector at startup.

docker-compose.yml
services:
coros-connector:
environment:
EXCLUDE_ACTIVITY_TYPES: triathlon,multiSport

Dreeve cannot import a multi-sport .fit file: it records the import as failed once and moves on. Excluding triathlon and multiSport keeps that out of your import history.

A filtered-out activity is not downloaded and not recorded, so allowing its sport again delivers it on the next sync, as long as it is within LOOKBACK_DAYS. Each sync logs every activity it filtered out and counts them in its summary, including with DRY_RUN.

Name Code Name Code
run 100 boatFishingLure 707
indoorRun 101 shoreFishingLure 708
trailRun 102 pondFishingLure 709
trackRun 103 kayakFishingLure 710
hike 104 inshoreFishing 711
mtnClimb 105 offshoreFishing 712
climb 106 boatFlyFishing 713
bike 200 shoreFlyFishing 714
indoorBike 201 surfFishing 715
roadEbike 202 indoorClimb 800
gravelRoadBike 203 bouldering 801
mountainRiding 204 outdoorClimb 802
mountainEbike 205 walk 900
helmetBike 299 jumpRope 901
poolSwim 300 climbStairs 902
openWater 301 elliptical 903
gymCardio 400 yoga 904
gpsCardio 401 pilates 905
strength 402 boxing 906
ski 500 badminton 1000
snowboard 501 tableTennis 1001
xcSki 502 basketball 1002
skiTouring 503 football 1003
row 700 pickleball 1004
indoorRow 701 tennis 1005
whitewater 702 padel 1006
flatwater 704 frisbee 1100
windsurfing 705 skateboarding 1101
speedsurfing 706 hyrox 1200
triathlon 10000 multiSport 10001
skiTouringOld 10002 multiPitch 10003
customSport 98 and 9800–9930

customSport matches every sport you defined yourself. A code that is not in the table, for a sport COROS added since, works too.

login reads COROS_EMAIL and COROS_PASSWORD, or their _FILE variants, and prompts in a terminal for whatever is missing. Without a terminal, as in a script, drop -it and set both, or login exits with an error. If COROS reports a region the connector does not know, set COROS_REGION.

With Docker secrets:

docker-compose.yml
services:
coros-connector:
environment:
COROS_EMAIL_FILE: /run/secrets/coros_email
COROS_PASSWORD_FILE: /run/secrets/coros_password
secrets: [coros_email, coros_password]
secrets:
coros_email:
file: ./coros/email.txt
coros_password:
file: ./coros/password.txt

Set these under environment: on the coros-connector service.

Variable Default What it does
COROS_EMAIL, COROS_PASSWORD - Account credentials, read only by login.
COROS_EMAIL_FILE, COROS_PASSWORD_FILE - Files holding them, such as Docker secrets.
COROS_REGION - us, eu or cn, when COROS reports a region the connector does not know.
WATCH_DIR /watch Dreeve’s watch folder.
STATE_DIR /state Where the ledger is stored. Mount it as a volume.
TOKENS_DIR /tokens Where the token is stored. Mount it as a volume.
POLL_INTERVAL 3600 Seconds between syncs, 900 to 604800, randomised by ±10%.
SINCE -30d How far back the first sync goes: -<days>d or YYYY-MM-DD.
LOOKBACK_DAYS 7 Days each sync lists again, for activities that reach COROS late.
MAX_DOWNLOADS_PER_CYCLE 25 Downloads per sync; the rest wait for the next one.
DOWNLOAD_DELAY_SECONDS 2 Pause between downloads.
MAX_BACKOFF_SECONDS 21600 Longest wait after repeated HTTP 429 responses (6 hours).
MAX_ATTEMPTS 5 Failed downloads before an activity is given up.
ACTIVITY_TYPES - Sports to sync, all when unset. See Choosing which sports are synced.
EXCLUDE_ACTIVITY_TYPES - Sports never to sync.
WRITE_TITLE true Write the COROS activity name into each .fit file so Dreeve shows it.
DRY_RUN false Log what would be delivered and write nothing.
TZ UTC Time zone that decides what “today” is.
UMASK 022 Permission bits removed from every file written.
HTTP_ADDR :8080 Health server address.
LOG_LEVEL info debug, info, warn or error.
LOG_FORMAT text text or json.
Command What it does
run Default. Syncs at startup, then every POLL_INTERVAL, and serves /healthz and /status.
sync-once Syncs once, then exits.
login Logs in to COROS and saves the token.
healthcheck Exits 0 when /healthz is healthy; the image’s HEALTHCHECK runs it.
version Prints the image’s version.
Terminal window
# Log in. Once, interactively.
> docker compose run --rm -it coros-connector login
# Run a single sync and exit.
> docker compose run --rm coros-connector sync-once
# Print the image's version.
> docker compose run --rm coros-connector version
  • The ledger in /state records every activity, so nothing is delivered twice, even across restarts and upgrades.
  • Files appear in /watch atomically, only once fully downloaded and checked to be an intact .fit file.
  • A failed download is retried on later syncs and recorded as failed after MAX_ATTEMPTS. Activities without a .fit file are recorded as no-file.
  • When COROS answers HTTP 429, the connector backs off from 15 minutes, doubling up to MAX_BACKOFF_SECONDS, and keeps that backoff across restarts.
  • With WRITE_TITLE=true, the COROS activity name is appended to the file as a workout message, unless the file already has one. Nothing COROS wrote is changed.
  • Passwords and tokens are redacted from every log line.
Terminal window
> docker compose pull coros-connector
> docker compose up -d coros-connector

No new login is needed and no activity is delivered twice. Read the release notes first: until 1.0.0, a minor version may change the configuration or the volumes, so pin the 0.Y tag to receive only fixes. To roll back, set the previous exact version as the image: tag.

The health server answers on port 8080 inside the container:

Endpoint Response
/healthz 200 when healthy, 503 while the token is rejected, missing or unreadable.
/status JSON with healthy, token, lastCycle and the ledger’s counts.

The container is unhealthy: the token was revoked, usually by a login elsewhere. Run login again; the running container picks it up on its next sync.

It fails at startup with a uid and gid: the volumes are not writable by user:. Fix their owner.

Retrying a failed or no-file activity: stop the connector, delete its entry from ./coros/state/ledger.json, start it again.