From da40b893606da315afda1ea2056854b001061800 Mon Sep 17 00:00:00 2001 From: Elias Sturim <267715325+sturimcode@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:05:19 -0400 Subject: [PATCH] Restructure the README around the reader's path - Fix the tagline: it said macOS and Windows while a whole section documented headless Linux - Document the missing flags (--history, --headless) and point at --help for --version/--config/--db - Group the Usage list by task instead of one flat pile - Move "How Garmin login works" below install and usage; new users hit Cloudflare lore before the install command - Split Install into macOS / Windows / Linux with uv as the lead path - Make Automatic sync platform-neutral with per-OS bullets - Fold the redundant Updating section into Usage (it said the same thing three times) - Merge Credential storage into Security as a per-platform list - Reorder Known quirks: the app-must-process-weigh-in quirk affects everyone, resolution rounding affects few - Drop the Stability table column (every row said Stable) Co-Authored-By: Claude Fable 5 --- README.md | 114 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 4017227..54e9393 100644 --- a/README.md +++ b/README.md @@ -7,40 +7,28 @@ Syncs body composition from a Eufy smart scale to Garmin Connect and Strava. -> macOS and Windows. 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 +> macOS, Windows, and headless Linux. Needs Python 3.12+ and a terminal. 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 +## What syncs -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](https://github.com/matin/garth) was [deprecated](https://github.com/matin/garth/discussions/222) and stays that way. - -eufy-sync logs in through [python-garminconnect](https://github.com/cyberjunky/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. +| Target | What syncs | +|--------|------------| +| Garmin Connect | Full body composition: weight, body fat, muscle mass, bone mass, hydration, BMR, visceral fat, metabolic age | +| Strava | Weight | ## 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. +Each block below can be pasted in whole; the lines run one after another. -**If you have [Homebrew](https://brew.sh/):** +### macOS -```bash -brew install pipx -pipx ensurepath -pipx install eufy-sync -``` +New to the terminal? Press Cmd+Space, type "terminal", hit Enter. -**If you don't, or aren't sure:** use [uv](https://docs.astral.sh/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): +The recommended installer is [uv](https://docs.astral.sh/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): ```bash curl -LsSf https://astral.sh/uv/install.sh | sh @@ -48,21 +36,37 @@ source $HOME/.local/bin/env uv tool install eufy-sync ``` -**On Windows:** open PowerShell (press Start, type "powershell", hit Enter) and install uv the same way: +Prefer [Homebrew](https://brew.sh/)? + +```bash +brew install pipx +pipx ensurepath +pipx install eufy-sync +``` + +### Windows + +Open PowerShell (press Start, type "powershell", hit Enter) and install uv: ```powershell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -Open a fresh terminal so the `uv` command is found, then run `uv tool install eufy-sync`. +Open a fresh PowerShell so the `uv` command is found, then run `uv tool install eufy-sync`. -Then open a new terminal window, so it picks up the newly installed command, and run: +### Linux + +Same uv commands as macOS. Setting up a server? See [Headless Linux](#headless-linux-server-or-vps) for the scheduling recipe. + +### First run + +Open a new terminal window, so it picks up the newly installed command, and run: ```bash 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. +It walks you through choosing targets and entering credentials, then runs the first sync. > **Cloned the repo?** Run install commands from outside the repo directory to avoid path conflicts, e.g. `cd /tmp && pipx install eufy-sync`. @@ -71,37 +75,41 @@ First run walks you through choosing targets and entering credentials, then runs ```bash 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 --history # recent sync history (a number shows more: --history 30) eufy-sync --dry-run # preview without uploading -eufy-sync --update # update to the latest version +eufy-sync --doctor # check the whole setup and print fixes for anything wrong +eufy-sync --verbose # detailed logs + +# accounts and profiles 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 + +# automation 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 --headless # never prompt; fail with a reauth message instead (for scheduled runs) + +# maintenance +eufy-sync --update # update to the latest version +eufy-sync --backfill-days 30 # sync the last 30 days 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 +eufy-sync --uninstall # remove all data and clean up ``` -## Updating +`eufy-sync --help` lists the rest (`--version`, `--config`, `--db`). -eufy-sync checks weekly and tells you when a new version is out. To update: - -```bash -eufy-sync --update -``` +eufy-sync checks PyPI weekly and says so when a new version is out; `eufy-sync --update` installs it, whichever installer you used. ## Automatic sync -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`. +On first run you can opt into syncing every 4 hours in the background: weigh yourself, come back later, and it has synced on its own. Logs go to `~/.garmin-sync/sync.log`, a notification tells you when a run fails, and `eufy-sync --uninstall-agent` turns it off. -If [terminal-notifier](https://github.com/julienXX/terminal-notifier) is installed (`brew install terminal-notifier`), clicking a failure notification opens Terminal with the fix command already running. Without it, notifications still appear; the click just does nothing useful. - -On Windows the same option registers a Scheduled Task that runs every 4 hours with no visible window. When a run fails, a toast notification names the command to fix it. +- **macOS** runs it as a Launch Agent. If [terminal-notifier](https://github.com/julienXX/terminal-notifier) is installed (`brew install terminal-notifier`), clicking a failure notification opens Terminal with the fix command already running. Without it, notifications still appear; the click just does nothing useful. +- **Windows** registers a Scheduled Task that runs with no visible window. When a run fails, a toast notification names the command to fix it. +- **Linux** has no managed agent; use the systemd timer below. ## Headless Linux (server or VPS) @@ -150,7 +158,7 @@ If Garmin is already set up and you want Strava: ## How it works ``` -Eufy Cloud -> eufy_client.py -> transform -> garmin_client.py -> Garmin (body comp) +Eufy Cloud -> eufy_client.py -> transform -> garmin_client.py -> Garmin (body comp) (pull) (auth) (filter, -> strava_client.py -> Strava (weight) dedup, state.db) @@ -158,21 +166,33 @@ Eufy Cloud -> eufy_client.py -> transform -> garmin_client.py -> Garmin 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: full body composition to Garmin through python-garminconnect's upload API (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. +## 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](https://github.com/matin/garth) was [deprecated](https://github.com/matin/garth/discussions/222) and stays that way. + +eufy-sync logs in through [python-garminconnect](https://github.com/cyberjunky/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. + ## 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`. On Windows the credential vault lives in Windows Credential Manager, and the file fallback relies on your user profile's permissions since Windows does not honor POSIX file modes. +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. -### Credential storage +Where passwords and OAuth tokens live: -By default, credentials go into one keychain item, so macOS only asks to "Always Allow" once, not once per secret. The keychain is used whenever it works. Run `eufy-sync --use-file-store` to switch permanently to a `600` file with no keychain prompts at all, a good fit for headless or scheduled setups; run `eufy-sync --use-keychain` to move them back. A credentials file that was not created by `--use-file-store` does not override a working keychain. Systems without a keychain (headless Linux) use the file automatically. +- **macOS:** a single Keychain item, not plaintext files. One item means macOS asks to "Always Allow" once, not once per secret. +- **Windows:** Windows Credential Manager. +- **Headless Linux, or after `--use-file-store`:** a single `600` file at `~/.garmin-sync/credentials.json`. + +The keychain is used whenever it works; systems without one fall back to the file automatically. `eufy-sync --use-file-store` makes the switch permanent, with no keychain prompts at all, a good fit for headless or scheduled setups; `eufy-sync --use-keychain` moves them back. A credentials file that was not created by `--use-file-store` does not override a working keychain. On Windows the file fallback relies on your user profile's permissions, since Windows does not honor POSIX file modes. + +The config in `~/.garmin-sync/` holds only email addresses and Strava app credentials, at `600` permissions. ## 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. +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. 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. +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. ## Tests