Sync Eufy smart scale body composition data to Garmin Connect and Strava
Project description
eufy-sync
Syncs body composition from a Eufy smart scale to Garmin Connect and Strava.
macOS only. Needs Python 3.12+ and a terminal.
Sync targets
| Target | What syncs | Stability |
|---|---|---|
| Garmin Connect | Full body composition: weight, body fat, muscle mass, bone mass, hydration, BMR, visceral fat, metabolic age | Stable |
| Strava | Weight | Stable |
Why
Eufy scales sync to Apple Health, Fitbit, and Google Fit, but not Garmin or Strava. If you train on either, your body comp is stuck in a separate app. This fixes that.
How Garmin login works
Garmin has no official API for writing body composition into Connect. In March 2026 it put Cloudflare in front of its login, which broke the Python libraries that talked to it; garth was deprecated and stays that way.
eufy-sync logs in through python-garminconnect, which gets past Cloudflare without a browser. On first run you enter your Garmin email and password, plus a code if you use two-factor. The tokens save to your keychain and refresh on their own, so later runs need no login. If that direct login gets rate-limited, a Chromium window opens once for you to sign in, then it continues. Headless setups use the direct login only, since there is no screen for a browser.
Install
You need a Eufy scale with cloud sync and a Garmin Connect and/or Strava account.
New to the terminal? Press Cmd+Space, type "terminal", hit Enter. Each block below can be pasted in whole; the lines run one after another.
If you have Homebrew:
brew install pipx
pipx ensurepath
pipx install eufy-sync
If you don't, or aren't sure: use uv. One paste, nothing to install first, and it fetches a compatible Python on its own (so a Python older than 3.12 is fine too):
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
uv tool install eufy-sync
Then open a new terminal window, so it picks up the newly installed command, and run:
eufy-sync
First run walks you through choosing targets and entering credentials, then runs the first sync. Whichever installer you used, eufy-sync --update handles updates from then on.
Cloned the repo? Run install 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 # last sync + token health
eufy-sync --doctor # check the whole setup and print fixes for anything wrong
eufy-sync --dry-run # preview without uploading
eufy-sync --update # update to the latest version
eufy-sync --setup-strava # add Strava
eufy-sync --select-profile # pick your profile on a shared scale
eufy-sync --reauth [target] # re-login (all, or garmin / strava)
eufy-sync --update-password # change stored passwords
eufy-sync --backfill-days 30 # sync the last 30 days
eufy-sync --verbose # detailed logs
eufy-sync --install-agent # turn automatic sync on
eufy-sync --uninstall-agent # turn automatic sync off
eufy-sync --uninstall # remove all data and clean up
eufy-sync --use-file-store # store credentials in a 0o600 file, no keychain prompts
eufy-sync --use-keychain # move credentials back into the system keychain
Updating
eufy-sync checks weekly and tells you when a new version is out. To update:
eufy-sync --update
Automatic sync (macOS)
On first run you can opt into syncing every 4 hours. If you do, a macOS Launch Agent runs it in the background: weigh yourself, open your laptop later, and it syncs on its own. Logs go to ~/.garmin-sync/sync.log, and you get a notification if something fails. Turn it off with eufy-sync --uninstall-agent.
Headless Linux (server or VPS)
eufy-sync runs on Linux too, and a server is a good home for it: no laptop that has to be awake. Without a system keychain, credentials fall back to a file with 600 permissions.
Set it up once over SSH with a plain eufy-sync run (the Garmin login and any two-factor code work in the terminal). Then schedule it with a systemd user timer:
# ~/.config/systemd/user/eufy-sync.service
[Unit]
Description=eufy-sync
[Service]
Type=oneshot
ExecStart=%h/.local/bin/eufy-sync --headless
# ~/.config/systemd/user/eufy-sync.timer
[Unit]
Description=Run eufy-sync every 4 hours
[Timer]
OnBootSec=5min
OnUnitActiveSec=4h
[Install]
WantedBy=timers.target
systemctl --user enable --now eufy-sync.timer
If a login expires later, the sync fails with a message naming the --reauth command; run it over SSH the same way. The one Eufy quirk hits hardest here: the cloud only has data after the phone app has processed the weigh-in (see Known quirks).
Adding Strava
If Garmin is already set up and you want Strava:
- Create a Strava API app 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
How it works
Eufy Cloud -> eufy_client.py -> transform -> garmin_client.py -> Garmin (FIT)
(pull) (auth) (filter, -> strava_client.py -> Strava (weight)
dedup,
state.db)
On each run it pulls your Eufy history and checks a local SQLite DB for what each target already has, then uploads only what is new: a FIT file to Garmin (skipping dates Garmin already holds, so two machines do not double up), and the latest weight to Strava. Every sync is recorded in the DB.
Security
Passwords and OAuth tokens live in a single item in your macOS Keychain, not plaintext files. The config in ~/.garmin-sync/ holds only email addresses and Strava app credentials, at 600 permissions. Credentials go over HTTPS to Eufy, Garmin, and Strava only, and are never logged or sent anywhere else. The one other outbound call is a weekly version check to pypi.org, with no credentials. On a host without a keychain (headless Linux), or if you choose --use-file-store, credentials fall back to a single 600 file at ~/.garmin-sync/credentials.json.
Credential storage
By default, credentials go into one keychain item, so macOS only asks to "Always Allow" once, not once per secret. Run eufy-sync --use-file-store to keep credentials in a 600 file instead, with no keychain prompts at all - a good fit for headless or scheduled setups. Run eufy-sync --use-keychain to move them back.
Known quirks
The Eufy cloud reports weight at about 0.05 kg resolution, so it can differ from the Eufy app, which may read Bluetooth at higher precision. Most days match within 0.1 lb; some can be off by up to ~0.5 lb, and the kg-to-lb conversion on Garmin adds a little rounding.
If more than one person uses the same Eufy account, setup asks which profile is yours, so only your weigh-ins sync. Until you choose, a sync that sees several profiles stops and lists them rather than guessing. Set it up before this existed? Run eufy-sync --select-profile once, then eufy-sync --backfill-days 30 to pull any of your weigh-ins an earlier version skipped.
The Eufy cloud only returns a weigh-in after the Eufy app has processed it. If you step on the scale and a sync finds nothing, open the app once so it uploads, then run eufy-sync again. The tool cannot trigger that upload itself, so it shows up most on headless or scheduled setups.
Tests
pytest tests/ -v
Disclaimer
Uses unofficial APIs for Eufy and Garmin, and the official Strava API. Could break if any of them change things. Use at your own risk.
Project details
Release history Release notifications | RSS feed
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.7.15.tar.gz.
File metadata
- Download URL: eufy_sync-1.7.15.tar.gz
- Upload date:
- Size: 68.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2dc9edf54cea0d3b42189505c45558cae46e8e4d436f3e161d55fd042f1e124
|
|
| MD5 |
622dfe3c07b0eda37e4ffc2463dccf96
|
|
| BLAKE2b-256 |
351f0f1fec20a80948056264e020ce212ea41f0acd6d395d745a1ada15c724d6
|
Provenance
The following attestation bundles were made for eufy_sync-1.7.15.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.7.15.tar.gz -
Subject digest:
b2dc9edf54cea0d3b42189505c45558cae46e8e4d436f3e161d55fd042f1e124 - Sigstore transparency entry: 2061525243
- Sigstore integration time:
-
Permalink:
sturimcode/eufy-sync@afd42e37f2b7d09ec4e2c4b70ac0d3c62507b040 -
Branch / Tag:
refs/tags/v1.7.15 - Owner: https://github.com/sturimcode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@afd42e37f2b7d09ec4e2c4b70ac0d3c62507b040 -
Trigger Event:
push
-
Statement type:
File details
Details for the file eufy_sync-1.7.15-py3-none-any.whl.
File metadata
- Download URL: eufy_sync-1.7.15-py3-none-any.whl
- Upload date:
- Size: 49.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72daa22f0613a5608b425656f454da826d3462a60168fad4445340a5597a6f65
|
|
| MD5 |
c5cdcfa795f82144ff24651a259fb7df
|
|
| BLAKE2b-256 |
9f5b062e00e0239093976fdb0dfce2ed8b281f527ad8e97efaf771e76be1cb68
|
Provenance
The following attestation bundles were made for eufy_sync-1.7.15-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.7.15-py3-none-any.whl -
Subject digest:
72daa22f0613a5608b425656f454da826d3462a60168fad4445340a5597a6f65 - Sigstore transparency entry: 2061525390
- Sigstore integration time:
-
Permalink:
sturimcode/eufy-sync@afd42e37f2b7d09ec4e2c4b70ac0d3c62507b040 -
Branch / Tag:
refs/tags/v1.7.15 - Owner: https://github.com/sturimcode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@afd42e37f2b7d09ec4e2c4b70ac0d3c62507b040 -
Trigger Event:
push
-
Statement type: