Sync Eufy smart scale body composition data to Garmin Connect and Strava
Project description
eufy-sync
Syncs body composition data from a Eufy smart scale to Garmin Connect and/or Strava. Weight, body fat %, muscle mass, bone mass, hydration, BMR, visceral fat, and metabolic age all come through to Garmin. Strava gets weight updates.
macOS only. Requires Python 3.9+ and a terminal. Setup is guided - you just answer a few prompts.
The problem
Eufy scales sync to Apple Health, Fitbit, and Google Fit - but not Garmin or Strava. If you use either for training, your body comp data is stuck in a separate app. This fixes that.
Why not just use python-garminconnect?
Every Python library that talked to Garmin broke in March 2026. Garmin put Cloudflare in front of their SSO, which blocks any login that doesn't come from a real browser. garth is deprecated, python-garminconnect can't authenticate anymore, and there's no official API.
This project gets around it with Playwright. On first run, a real Chromium window opens and you log in normally. OAuth2 tokens get saved to your system keychain and refresh on their own for about a year. After that first login, no browser needed - body comp data goes up as FIT files through Garmin's upload endpoint.
Install
You need Python 3.9+, a Eufy scale with cloud sync, and a Garmin Connect and/or Strava account.
First, install pipx if you don't have it:
brew install pipx
Or if you don't use Homebrew: pip3 install pipx
Then install and run:
pipx install eufy-sync
eufy-sync
Setup is guided on first run - choose your sync targets (Garmin, Strava, or both), enter your credentials, and your data syncs automatically.
Note: If you've cloned this repo, run pipx commands from outside the repo directory to avoid path conflicts (e.g.,
cd /tmp && pipx install eufy-sync).
Usage
eufy-sync # sync new measurements to all configured targets
eufy-sync --status # check last sync + token health
eufy-sync --dry-run # preview without uploading
eufy-sync --setup-strava # connect Strava (add to existing setup)
eufy-sync --reauth # re-login to all targets
eufy-sync --reauth garmin # re-login to Garmin only
eufy-sync --reauth strava # re-authorize Strava only
eufy-sync --update-password # change stored passwords
eufy-sync --backfill-days 30 # sync last 30 days
eufy-sync --verbose # show detailed sync logs
eufy-sync --install-agent # set up automatic sync
eufy-sync --uninstall-agent # remove automatic sync
eufy-sync --uninstall # remove all data and clean up
Updating
The tool checks for updates weekly and will let you know when a new version is available. To update:
pipx install --force eufy-sync
Automatic sync (macOS)
On first run, you'll be asked if you want to sync automatically every 4 hours. If you say yes, a macOS Launch Agent is installed that runs in the background - weigh yourself, open your laptop later, and it syncs on its own.
Logs go to ~/.garmin-sync/sync.log. You get a macOS notification if something fails.
To disable: eufy-sync --uninstall-agent
Adding Strava
If you already have Garmin set up and want to add Strava:
- Create a Strava API application at https://www.strava.com/settings/api
- Set 'Authorization Callback Domain' to
localhost - Run
eufy-sync --setup-stravaand enter your Client ID and Secret - Authorize in the browser when it opens
Future syncs will update both Garmin and Strava automatically. Strava receives weight only (no body composition - Strava's API limitation).
How it works
/--> garmin_client.py --> Garmin Connect
Eufy Cloud API --> eufy_client.py --> transform.py (FIT file + upload)
(fetch history) (auth + pull) (filter, dedup) \
| \--> strava_client.py --> Strava
state.db (weight update)
(sync watermark)
- Authenticate to Eufy cloud API, pull measurement history
- Check local SQLite DB for what's already been synced (per target)
- For Garmin: check for existing entries on the same date (multi-machine dedup)
- Generate a FIT binary file and upload to Garmin Connect
- Update athlete weight on Strava
- Record syncs in DB
Security
Your passwords and OAuth tokens are stored in your system keychain (macOS Keychain) - not in plaintext files. Config files in ~/.garmin-sync/ only contain email addresses and Strava API app credentials, with 600 permissions. Credentials are only sent to Eufy, Garmin, and Strava's own servers over HTTPS. They are never logged, uploaded, or transmitted anywhere else. The only other outbound call is a weekly version check to pypi.org (no credentials sent). You can verify this yourself - the codebase is small and the outbound calls are in eufy_client.py, garmin_auth.py, strava_client.py, and the update checker in cli.py.
On systems without keychain support (headless Linux), credentials fall back to file-based storage with 600 permissions.
Known quirks
Weight precision: The Eufy cloud API returns weight at ~0.05 kg resolution, which can differ slightly from what the Eufy app displays (the app may read from Bluetooth/local storage with higher precision). In testing, most days match within 0.1 lbs, but occasional readings can be off by up to ~0.5 lbs. If Garmin displays in lbs, the kg-to-lbs conversion adds a bit more rounding on top.
Tests
pytest tests/ -v
Disclaimer
Uses unofficial APIs for Eufy and Garmin, and the official Strava API. Could break if any of these companies change things. Use at your own risk.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file eufy_sync-1.5.2.tar.gz.
File metadata
- Download URL: eufy_sync-1.5.2.tar.gz
- Upload date:
- Size: 38.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a86fe565846de2c6cfa96f09110e0884d04657a14b5827829977a2328b4f418
|
|
| MD5 |
b1733020d0ad56fa8d74cfa2ba92bc35
|
|
| BLAKE2b-256 |
d7606ad341f2432b926584798f3b1a0ccd53f08f468490ea6d38a42b23cb13c0
|
Provenance
The following attestation bundles were made for eufy_sync-1.5.2.tar.gz:
Publisher:
publish.yml on sturimcode/eufy-sync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
eufy_sync-1.5.2.tar.gz -
Subject digest:
8a86fe565846de2c6cfa96f09110e0884d04657a14b5827829977a2328b4f418 - Sigstore transparency entry: 1217940568
- Sigstore integration time:
-
Permalink:
sturimcode/eufy-sync@167b34fca7d019a3e5a3cda127815da089fb64fc -
Branch / Tag:
refs/tags/v1.5.2 - Owner: https://github.com/sturimcode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@167b34fca7d019a3e5a3cda127815da089fb64fc -
Trigger Event:
release
-
Statement type:
File details
Details for the file eufy_sync-1.5.2-py3-none-any.whl.
File metadata
- Download URL: eufy_sync-1.5.2-py3-none-any.whl
- Upload date:
- Size: 34.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e1785c9aaec0b4a9ac8fb6e4c4f832348388650ac94a649e320515e5160c46c
|
|
| MD5 |
4b3c2f4a747e12c7bb8f41410041b193
|
|
| BLAKE2b-256 |
b21809d35157b5ecab6e7c33a9dd6ee6e19b38195e7864e9eaf2c65c3773ed18
|
Provenance
The following attestation bundles were made for eufy_sync-1.5.2-py3-none-any.whl:
Publisher:
publish.yml on sturimcode/eufy-sync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
eufy_sync-1.5.2-py3-none-any.whl -
Subject digest:
4e1785c9aaec0b4a9ac8fb6e4c4f832348388650ac94a649e320515e5160c46c - Sigstore transparency entry: 1217940582
- Sigstore integration time:
-
Permalink:
sturimcode/eufy-sync@167b34fca7d019a3e5a3cda127815da089fb64fc -
Branch / Tag:
refs/tags/v1.5.2 - Owner: https://github.com/sturimcode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@167b34fca7d019a3e5a3cda127815da089fb64fc -
Trigger Event:
release
-
Statement type: