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 itThe 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.
1. Add the container
Section titled “1. Add the container”Add this alongside the app and daemon services in your
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-networkImages 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.
2. Create the folders
Section titled “2. Create the folders”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:
> mkdir -p watch coros/state coros/tokens> sudo chown 1000:1000 watch coros/state coros/tokens3. Log in
Section titled “3. Log in”> docker compose run --rm -it coros-connector loginThis 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.
4. Start it
Section titled “4. Start it”> docker compose up -d coros-connector> docker compose logs -f coros-connectorA 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.
Why the first sync is spread out
Section titled “Why the first sync is spread out”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.
Choosing which sports are synced
Section titled “Choosing which sports are synced”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_TYPESsyncs only the sports it lists.EXCLUDE_ACTIVITY_TYPESnever 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.
services: coros-connector: environment: EXCLUDE_ACTIVITY_TYPES: triathlon,multiSportDreeve 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.
Logging in without a terminal
Section titled “Logging in without a terminal”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:
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.txtConfiguration
Section titled “Configuration”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. |
Commands
Section titled “Commands”| 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. |
# 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 versionHow it works
Section titled “How it works”- The ledger in
/staterecords every activity, so nothing is delivered twice, even across restarts and upgrades. - Files appear in
/watchatomically, only once fully downloaded and checked to be an intact.fitfile. - A failed download is retried on later syncs and recorded as
failedafterMAX_ATTEMPTS. Activities without a.fitfile are recorded asno-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 aworkoutmessage, unless the file already has one. Nothing COROS wrote is changed. - Passwords and tokens are redacted from every log line.
Upgrading
Section titled “Upgrading”> docker compose pull coros-connector> docker compose up -d coros-connectorNo 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.
When something looks wrong
Section titled “When something looks wrong”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.