Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Bug report
description: Report a setup or sync problem.
body:
- type: markdown
attributes:
value: |
Tell us what happened and how to reproduce it. Before sharing output, remove email addresses, profile IDs, health data, passwords, tokens, and API secrets. Do not upload files from your credential store.
- type: input
id: version
attributes:
label: eufy-sync version
description: Run `eufy-sync --version`, or say if installation failed.
validations:
required: true
- type: input
id: operating-system
attributes:
label: Operating system
description: Include the version and whether this runs on a desktop, server, or NAS.
placeholder: macOS 15, Windows 11, or Ubuntu 24.04 on a VPS
validations:
required: true
- type: dropdown
id: target
attributes:
label: Affected service or step
multiple: true
options:
- Eufy
- Garmin Connect
- Strava
- Zwift
- Installation or scheduling
- Not sure
validations:
required: true
- type: textarea
id: problem
attributes:
label: What happened?
description: Include the command or steps you tried, what you expected, and what happened instead.
validations:
required: true
- type: textarea
id: diagnostics
attributes:
label: Diagnostic output
description: Run `eufy-sync --doctor` if the command is available. Paste only the relevant output after removing personal data and secrets. If the command fails, the error message helps too.
render: text
177 changes: 33 additions & 144 deletions README.md

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Command reference

Running `eufy-sync` with no options syncs new measurements to every configured target.

## Check and preview

```bash
eufy-sync --status # last sync and token health
eufy-sync --history # the last 14 sync-history entries
eufy-sync --history 30 # choose how many entries to show
eufy-sync --dry-run # preview without uploading
eufy-sync --doctor # check the whole setup and print fixes
eufy-sync --verbose # show detailed logs
eufy-sync --version # show the installed version
```

## Accounts, targets, and profiles

```bash
eufy-sync --setup-strava # add or reconnect Strava
eufy-sync --setup-zwift # add experimental Zwift weight sync
eufy-sync --disconnect-zwift # remove Zwift and its saved login
eufy-sync --target garmin # sync one target (garmin, strava, or zwift)
eufy-sync --select-profile # choose a profile on a shared Eufy account
eufy-sync --reauth # log back into every configured target
eufy-sync --reauth garmin # log back into one target (garmin, strava, or zwift)
eufy-sync --update-password # change stored Eufy, Garmin, or Zwift passwords
```

On a fresh installation, run `eufy-sync` and choose any combination of Garmin, Strava, and experimental Zwift. The separate `--setup-zwift` command also supports a fresh Zwift-only installation, but does not offer automatic scheduling; run `eufy-sync --install-agent` afterward on macOS or Windows if needed.

## Automation and storage

```bash
eufy-sync --install-agent # enable four-hour syncs on macOS or Windows
eufy-sync --uninstall-agent # disable automatic sync
eufy-sync --headless # never prompt during a scheduled run
eufy-sync --use-file-store # move credentials to a local file and avoid keychain prompts
eufy-sync --use-keychain # move credentials back to the system keychain
```

Linux scheduling is covered in [Headless Linux](headless-linux.md). `--headless` tries to renew or restore expired sessions with stored credentials, but a service can still require interactive recovery later.

## History recovery

```bash
eufy-sync --backfill-days 30 # sync eligible measurements not recorded as delivered
eufy-sync --repair-days 30 # resend Garmin history even when recorded as delivered
```

`--backfill-days` sends measurements in the chosen window only when the local database says they have not reached that target. It is useful after adding a target or increasing the default seven-day lookback.

`--repair-days` is for Garmin history that was deleted from Garmin Connect or filed under the wrong date by eufy-sync versions before 1.9.0. It resends Garmin measurements in the window even when the local database says they were delivered. It still leaves alone dates eufy-sync never uploaded when Garmin already holds data from another source.

Delete wrong-dated entries in Garmin Connect before repair. eufy-sync cannot delete them, so otherwise they remain beside the corrected entries. Strava and Zwift store current weight rather than history; in repair mode they receive only the newest eligible current weight.

`--repair-days` and `--backfill-days` cannot be used together. Both can be limited to one target with `--target` and previewed with `--dry-run`.

## Maintenance and paths

```bash
eufy-sync --update # update to the latest release
eufy-sync --uninstall # remove saved data, credentials, and automatic sync
eufy-sync --config PATH # use another configuration file
eufy-sync --db PATH # use another sync database
```

The default config is `~/.garmin-sync/config.yaml`; the default database is `~/.garmin-sync/state.db`. `eufy-sync --help` is the source of truth for the options supported by your installed version.
77 changes: 77 additions & 0 deletions docs/headless-linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Headless Linux

eufy-sync can run on a Linux server or VPS, so syncing does not depend on a laptop being awake. Without a working system keychain, it stores credentials in `~/.garmin-sync/credentials.json` with `600` permissions.

## Set up the account

Install eufy-sync with [uv](https://docs.astral.sh/uv/):

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
uv tool install eufy-sync
```

Run `eufy-sync` once in an interactive SSH session. Setup lets you choose Garmin, Strava, and/or experimental Zwift. Garmin login and any two-factor code work in the terminal.

For Strava, forward the authorization callback port when connecting from your computer. Replace `user@server` with your SSH login:

```bash
ssh -L 8089:localhost:8089 user@server
```

Run setup in that session, then open the printed Strava authorization URL in your computer's browser while the command waits. The tunnel sends the browser's localhost callback to eufy-sync on the server.

Run `eufy-sync --doctor` before scheduling it. This catches missing credentials and configuration problems while you still have an interactive prompt.

## Add a systemd user timer

Create `~/.config/systemd/user/eufy-sync.service`:

```ini
[Unit]
Description=eufy-sync

[Service]
Type=oneshot
ExecStart=%h/.local/bin/eufy-sync --headless
```

Create `~/.config/systemd/user/eufy-sync.timer`:

```ini
[Unit]
Description=Run eufy-sync every 4 hours

[Timer]
OnBootSec=5min
OnUnitActiveSec=4h

[Install]
WantedBy=timers.target
```

Load and start the timer:

```bash
systemctl --user daemon-reload
systemctl --user enable --now eufy-sync.timer
systemctl --user status eufy-sync.timer
```

A user timer normally runs only while that user's systemd manager is active. If this server must sync after you log out and following a reboot, ask an administrator to enable lingering for the account:

```bash
sudo loginctl enable-linger "$USER"
```

## Check a scheduled run

```bash
systemctl --user start eufy-sync.service
journalctl --user -u eufy-sync.service --since today
```

Scheduled runs normally renew sessions or log back in with stored credentials. A service can still change or reject its login flow, and Garmin may require a security code. When that happens, inspect the journal, run the recovery command it names over SSH, and then start the service again.

Eufy may provide a raw Wi-Fi weight before the phone app processes the full body composition. If a scheduled run sends only weight, open the Eufy app, let it process the weigh-in, and run the service again. More recovery steps are in [Security and troubleshooting](security-and-troubleshooting.md).
89 changes: 89 additions & 0 deletions docs/security-and-troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Security, troubleshooting, and how it works

Start with:

```bash
eufy-sync --doctor
```

It checks configuration, credentials, target authorization, the local database, and automatic sync, then prints a specific command for anything it can repair.

## Credential storage

Credentials travel over HTTPS to their respective services: Eufy, Garmin, Strava, and Zwift. They are never logged or sent elsewhere. The only other outbound request is a weekly version check to `pypi.org`, without credentials.

Passwords and OAuth tokens are stored in one place:

- **macOS:** one Keychain item, so macOS can grant access to the group instead of prompting once per secret.
- **Windows:** Windows Credential Manager.
- **Headless Linux, or after `--use-file-store`:** `~/.garmin-sync/credentials.json`, with `600` permissions on systems that support POSIX modes.

The keychain is used whenever it works. Systems without one fall back to the file automatically. `eufy-sync --use-file-store` moves existing credentials to the file and keeps using it; `eufy-sync --use-keychain` moves them back. A credentials file that was not adopted with `--use-file-store` does not override a working keychain. On Windows, file fallback relies on the user profile's permissions because Windows does not honor POSIX file modes.

`~/.garmin-sync/config.yaml` contains email addresses, the selected Eufy profile ID, and the public Strava client ID. It is written with `600` permissions on systems that support POSIX modes. Passwords and the Strava client secret remain in the credential store.

## Garmin login recovery

Garmin has no official API for writing body composition into Connect. eufy-sync logs in through [python-garminconnect](https://github.com/cyberjunky/python-garminconnect), using your Garmin email, password, and a two-factor code when required. It stores the resulting tokens and refreshes them on later runs.

Garmin put Cloudflare in front of its login in March 2026, which broke the Python libraries that had talked to it. [garth was deprecated](https://github.com/matin/garth/discussions/222), so eufy-sync uses python-garminconnect's current login path instead.

If Garmin repeatedly reports rate-limit or Cloudflare errors while the Garmin app still works, run:

```bash
eufy-sync --reauth garmin
```

Stale tokens can produce the same errors, and a new login often clears them.

If direct login is rate-limited, an interactive installation can use a Chromium fallback. The error will name the matching command for your installer:

```bash
uv tool install --force 'eufy-sync[browser]'
```

```bash
pipx install --force 'eufy-sync[browser]'
```

The browser extra is optional because Playwright is much larger than the normal installation. Headless runs cannot open this fallback. They normally renew sessions or log back in from saved credentials, but no unattended login can be guaranteed after Garmin or another service changes its authentication flow.

## Missing or incomplete measurements

The Eufy cloud can return a raw Wi-Fi weigh-in before the phone app processes it. That gives eufy-sync a weight but not body fat, muscle mass, and the other Garmin metrics. If a recent weigh-in is missing or only weight appears, open the Eufy app, wait for it to process the record, then run `eufy-sync` again. eufy-sync cannot trigger that processing.

If several people share one Eufy account, eufy-sync stops instead of guessing which profile is yours. Choose it with:

```bash
eufy-sync --select-profile
eufy-sync --backfill-days 30
```

The Eufy cloud reports weight at about 0.05 kg resolution. This can differ from the Eufy app, which may read Bluetooth data at higher precision. Most measurements match within 0.1 lb; some can differ by up to about 0.5 lb, with a little more rounding when Garmin converts kilograms to pounds.

For missing Garmin history, see [History recovery](command-reference.md#history-recovery).

## Platform problems

On old Windows versions, including reported installations on Windows Server 2016, the uv installer download can fail when the system does not trust Let's Encrypt certificates. Update the operating system's root certificates, or install on a current Windows machine and copy the installed folder.

For a scheduled Linux run, inspect its journal:

```bash
journalctl --user -u eufy-sync.service --since today
```

The complete timer setup is in [Headless Linux](headless-linux.md).

## How a sync works

```text
Eufy Cloud -> eufy_client.py -> transform -> garmin_client.py -> Garmin (body comp)
(pull) (auth) (filter, -> strava_client.py -> Strava (weight)
dedup, -> zwift_client.py -> Zwift (weight)
state.db)
```

Each run pulls Eufy history and checks the local SQLite database for what each target has already received. Garmin gets new full body-composition records through python-garminconnect's upload API. Dates Garmin already holds are skipped, which helps avoid duplicates when two machines sync the same account. Strava and Zwift receive the latest eligible current weight. Successful deliveries are recorded in the database; Zwift is recorded only after eufy-sync reads the profile again and verifies the saved weight.

Garmin, Eufy, and Zwift use unofficial APIs in this project. Strava uses its official API. Changes to any service can require an eufy-sync update.
2 changes: 1 addition & 1 deletion eufy_sync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Sync Eufy smart scale body composition data to Garmin Connect and Strava."""

__version__ = "1.13.0"
__version__ = "1.13.1"

# Public API for programmatic use
from eufy_sync.eufy_client import EufyClient, EufyMeasurement
Expand Down
Loading