eufy-sync 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. eufy_sync-1.5.0/LICENSE +21 -0
  2. eufy_sync-1.5.0/PKG-INFO +152 -0
  3. eufy_sync-1.5.0/README.md +124 -0
  4. eufy_sync-1.5.0/eufy_sync/__init__.py +20 -0
  5. eufy_sync-1.5.0/eufy_sync/cli.py +832 -0
  6. eufy_sync-1.5.0/eufy_sync/config.py +132 -0
  7. eufy_sync-1.5.0/eufy_sync/credentials.py +75 -0
  8. eufy_sync-1.5.0/eufy_sync/eufy_client.py +225 -0
  9. eufy_sync-1.5.0/eufy_sync/fit.py +210 -0
  10. eufy_sync-1.5.0/eufy_sync/garmin_auth.py +421 -0
  11. eufy_sync-1.5.0/eufy_sync/garmin_client.py +102 -0
  12. eufy_sync-1.5.0/eufy_sync/state.py +104 -0
  13. eufy_sync-1.5.0/eufy_sync/strava_client.py +248 -0
  14. eufy_sync-1.5.0/eufy_sync/sync.py +135 -0
  15. eufy_sync-1.5.0/eufy_sync/transform.py +54 -0
  16. eufy_sync-1.5.0/eufy_sync.egg-info/PKG-INFO +152 -0
  17. eufy_sync-1.5.0/eufy_sync.egg-info/SOURCES.txt +31 -0
  18. eufy_sync-1.5.0/eufy_sync.egg-info/dependency_links.txt +1 -0
  19. eufy_sync-1.5.0/eufy_sync.egg-info/entry_points.txt +2 -0
  20. eufy_sync-1.5.0/eufy_sync.egg-info/requires.txt +5 -0
  21. eufy_sync-1.5.0/eufy_sync.egg-info/top_level.txt +1 -0
  22. eufy_sync-1.5.0/pyproject.toml +54 -0
  23. eufy_sync-1.5.0/setup.cfg +4 -0
  24. eufy_sync-1.5.0/tests/test_cli.py +161 -0
  25. eufy_sync-1.5.0/tests/test_eufy_client.py +48 -0
  26. eufy_sync-1.5.0/tests/test_fit.py +139 -0
  27. eufy_sync-1.5.0/tests/test_garmin_auth.py +65 -0
  28. eufy_sync-1.5.0/tests/test_retry.py +36 -0
  29. eufy_sync-1.5.0/tests/test_strava_client.py +140 -0
  30. eufy_sync-1.5.0/tests/test_summary.py +152 -0
  31. eufy_sync-1.5.0/tests/test_sync.py +125 -0
  32. eufy_sync-1.5.0/tests/test_transform.py +72 -0
  33. eufy_sync-1.5.0/tests/test_update_check.py +139 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elias Sturim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: eufy-sync
3
+ Version: 1.5.0
4
+ Summary: Sync Eufy smart scale body composition data to Garmin Connect and Strava
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/sturimcode/eufy-sync
7
+ Project-URL: Issues, https://github.com/sturimcode/eufy-sync/issues
8
+ Keywords: garmin,garmin connect,strava,eufy,eufylife,smart scale,body composition,fit file,weight sync
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: keyring>=25.0.0
24
+ Requires-Dist: playwright>=1.40.0
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: setuptools>=68.0
27
+ Dynamic: license-file
28
+
29
+ # eufy-sync
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/eufy-sync)](https://pypi.org/project/eufy-sync/)
32
+ [![Downloads](https://img.shields.io/pypi/dm/eufy-sync)](https://pypi.org/project/eufy-sync/)
33
+ ![Python](https://img.shields.io/pypi/pyversions/eufy-sync)
34
+ ![License](https://img.shields.io/badge/license-MIT-green)
35
+
36
+ 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.
37
+
38
+ > macOS only. Requires Python 3.9+ and a terminal. Setup is guided - you just answer a few prompts.
39
+
40
+ ## The problem
41
+
42
+ 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.
43
+
44
+ ## Why not just use python-garminconnect?
45
+
46
+ 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](https://github.com/matin/garth) is [deprecated](https://github.com/matin/garth/discussions/222), [python-garminconnect](https://github.com/cyberjunky/python-garminconnect) can't authenticate anymore, and there's no official API.
47
+
48
+ 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.
49
+
50
+ ## Install
51
+
52
+ You need Python 3.9+, a Eufy scale with cloud sync, and a Garmin Connect and/or Strava account.
53
+
54
+ First, install pipx if you don't have it:
55
+ ```bash
56
+ brew install pipx
57
+ ```
58
+ Or if you don't use Homebrew: `pip3 install pipx`
59
+
60
+ Then install and run:
61
+ ```bash
62
+ pipx install eufy-sync
63
+ eufy-sync
64
+ ```
65
+
66
+ Setup is guided on first run - choose your sync targets (Garmin, Strava, or both), enter your credentials, and your data syncs automatically.
67
+
68
+ > **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`).
69
+
70
+ ## Usage
71
+
72
+ ```bash
73
+ eufy-sync # sync new measurements to all configured targets
74
+ eufy-sync --status # check last sync + token health
75
+ eufy-sync --dry-run # preview without uploading
76
+ eufy-sync --setup-strava # connect Strava (add to existing setup)
77
+ eufy-sync --reauth # re-login to all targets
78
+ eufy-sync --reauth garmin # re-login to Garmin only
79
+ eufy-sync --reauth strava # re-authorize Strava only
80
+ eufy-sync --update-password # change stored passwords
81
+ eufy-sync --backfill-days 30 # sync last 30 days
82
+ eufy-sync --verbose # show detailed sync logs
83
+ eufy-sync --install-agent # set up automatic sync
84
+ eufy-sync --uninstall-agent # remove automatic sync
85
+ eufy-sync --uninstall # remove all data and clean up
86
+ ```
87
+
88
+ ## Updating
89
+
90
+ The tool checks for updates weekly and will let you know when a new version is available. To update:
91
+
92
+ ```bash
93
+ pipx install --force eufy-sync
94
+ ```
95
+
96
+ ## Automatic sync (macOS)
97
+
98
+ 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.
99
+
100
+ Logs go to `~/.garmin-sync/sync.log`. You get a macOS notification if something fails.
101
+
102
+ To disable: `eufy-sync --uninstall-agent`
103
+
104
+ ## Adding Strava
105
+
106
+ If you already have Garmin set up and want to add Strava:
107
+
108
+ 1. Create a Strava API application at https://www.strava.com/settings/api
109
+ 2. Set the redirect URI to `http://localhost:8089/callback`
110
+ 3. Run `eufy-sync --setup-strava` and enter your Client ID and Secret
111
+ 4. Authorize in the browser when it opens
112
+
113
+ Future syncs will update both Garmin and Strava automatically. Strava receives weight only (no body composition - Strava's API limitation).
114
+
115
+ ## How it works
116
+
117
+ ```
118
+ /--> garmin_client.py --> Garmin Connect
119
+ Eufy Cloud API --> eufy_client.py --> transform.py (FIT file + upload)
120
+ (fetch history) (auth + pull) (filter, dedup) \
121
+ | \--> strava_client.py --> Strava
122
+ state.db (weight update)
123
+ (sync watermark)
124
+ ```
125
+
126
+ 1. Authenticate to Eufy cloud API, pull measurement history
127
+ 2. Check local SQLite DB for what's already been synced (per target)
128
+ 3. For Garmin: check for existing entries on the same date (multi-machine dedup)
129
+ 4. Generate a FIT binary file and upload to Garmin Connect
130
+ 5. Update athlete weight on Strava
131
+ 6. Record syncs in DB
132
+
133
+ ## Security
134
+
135
+ 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`.
136
+
137
+ On systems without keychain support (headless Linux), credentials fall back to file-based storage with `600` permissions.
138
+
139
+ ## Known quirks
140
+
141
+ **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.
142
+
143
+ ## Tests
144
+
145
+ ```bash
146
+ pytest tests/ -v
147
+ ```
148
+
149
+ ## Disclaimer
150
+
151
+ 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.
152
+
@@ -0,0 +1,124 @@
1
+ # eufy-sync
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/eufy-sync)](https://pypi.org/project/eufy-sync/)
4
+ [![Downloads](https://img.shields.io/pypi/dm/eufy-sync)](https://pypi.org/project/eufy-sync/)
5
+ ![Python](https://img.shields.io/pypi/pyversions/eufy-sync)
6
+ ![License](https://img.shields.io/badge/license-MIT-green)
7
+
8
+ 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.
9
+
10
+ > macOS only. Requires Python 3.9+ and a terminal. Setup is guided - you just answer a few prompts.
11
+
12
+ ## The problem
13
+
14
+ 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.
15
+
16
+ ## Why not just use python-garminconnect?
17
+
18
+ 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](https://github.com/matin/garth) is [deprecated](https://github.com/matin/garth/discussions/222), [python-garminconnect](https://github.com/cyberjunky/python-garminconnect) can't authenticate anymore, and there's no official API.
19
+
20
+ 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.
21
+
22
+ ## Install
23
+
24
+ You need Python 3.9+, a Eufy scale with cloud sync, and a Garmin Connect and/or Strava account.
25
+
26
+ First, install pipx if you don't have it:
27
+ ```bash
28
+ brew install pipx
29
+ ```
30
+ Or if you don't use Homebrew: `pip3 install pipx`
31
+
32
+ Then install and run:
33
+ ```bash
34
+ pipx install eufy-sync
35
+ eufy-sync
36
+ ```
37
+
38
+ Setup is guided on first run - choose your sync targets (Garmin, Strava, or both), enter your credentials, and your data syncs automatically.
39
+
40
+ > **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`).
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ eufy-sync # sync new measurements to all configured targets
46
+ eufy-sync --status # check last sync + token health
47
+ eufy-sync --dry-run # preview without uploading
48
+ eufy-sync --setup-strava # connect Strava (add to existing setup)
49
+ eufy-sync --reauth # re-login to all targets
50
+ eufy-sync --reauth garmin # re-login to Garmin only
51
+ eufy-sync --reauth strava # re-authorize Strava only
52
+ eufy-sync --update-password # change stored passwords
53
+ eufy-sync --backfill-days 30 # sync last 30 days
54
+ eufy-sync --verbose # show detailed sync logs
55
+ eufy-sync --install-agent # set up automatic sync
56
+ eufy-sync --uninstall-agent # remove automatic sync
57
+ eufy-sync --uninstall # remove all data and clean up
58
+ ```
59
+
60
+ ## Updating
61
+
62
+ The tool checks for updates weekly and will let you know when a new version is available. To update:
63
+
64
+ ```bash
65
+ pipx install --force eufy-sync
66
+ ```
67
+
68
+ ## Automatic sync (macOS)
69
+
70
+ 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.
71
+
72
+ Logs go to `~/.garmin-sync/sync.log`. You get a macOS notification if something fails.
73
+
74
+ To disable: `eufy-sync --uninstall-agent`
75
+
76
+ ## Adding Strava
77
+
78
+ If you already have Garmin set up and want to add Strava:
79
+
80
+ 1. Create a Strava API application at https://www.strava.com/settings/api
81
+ 2. Set the redirect URI to `http://localhost:8089/callback`
82
+ 3. Run `eufy-sync --setup-strava` and enter your Client ID and Secret
83
+ 4. Authorize in the browser when it opens
84
+
85
+ Future syncs will update both Garmin and Strava automatically. Strava receives weight only (no body composition - Strava's API limitation).
86
+
87
+ ## How it works
88
+
89
+ ```
90
+ /--> garmin_client.py --> Garmin Connect
91
+ Eufy Cloud API --> eufy_client.py --> transform.py (FIT file + upload)
92
+ (fetch history) (auth + pull) (filter, dedup) \
93
+ | \--> strava_client.py --> Strava
94
+ state.db (weight update)
95
+ (sync watermark)
96
+ ```
97
+
98
+ 1. Authenticate to Eufy cloud API, pull measurement history
99
+ 2. Check local SQLite DB for what's already been synced (per target)
100
+ 3. For Garmin: check for existing entries on the same date (multi-machine dedup)
101
+ 4. Generate a FIT binary file and upload to Garmin Connect
102
+ 5. Update athlete weight on Strava
103
+ 6. Record syncs in DB
104
+
105
+ ## Security
106
+
107
+ 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`.
108
+
109
+ On systems without keychain support (headless Linux), credentials fall back to file-based storage with `600` permissions.
110
+
111
+ ## Known quirks
112
+
113
+ **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.
114
+
115
+ ## Tests
116
+
117
+ ```bash
118
+ pytest tests/ -v
119
+ ```
120
+
121
+ ## Disclaimer
122
+
123
+ 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.
124
+
@@ -0,0 +1,20 @@
1
+ """Sync Eufy smart scale body composition data to Garmin Connect and Strava."""
2
+
3
+ __version__ = "1.5.0"
4
+
5
+ # Public API for programmatic use
6
+ from eufy_sync.garmin_auth import GarminAuth
7
+ from eufy_sync.eufy_client import EufyClient, EufyMeasurement
8
+ from eufy_sync.fit import FitEncoder
9
+ from eufy_sync.strava_client import StravaClient
10
+ from eufy_sync.transform import GarminBodyComposition, transform
11
+
12
+ __all__ = [
13
+ "GarminAuth",
14
+ "EufyClient",
15
+ "EufyMeasurement",
16
+ "FitEncoder",
17
+ "GarminBodyComposition",
18
+ "StravaClient",
19
+ "transform",
20
+ ]