renpho-py 1.0.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.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 danvaneijck (original renpho-api)
4
+ Copyright (c) 2026 ChocoTonic (renpho-py and subsequent modifications)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
renpho_py-1.0.0/NOTICE ADDED
@@ -0,0 +1,16 @@
1
+ renpho-py
2
+ ========
3
+
4
+ This project began as a copy of the MIT-licensed `renpho-api`
5
+ (https://github.com/danvaneijck/renpho-api) by danvaneijck, and is now
6
+ independently maintained. The original license and copyright are preserved in
7
+ LICENSE.
8
+
9
+ The underlying Renpho cloud API was reverse-engineered; the approach and
10
+ protocol details are based on RenphoGarminSync-CLI
11
+ (https://github.com/forkerer/RenphoGarminSync-CLI).
12
+
13
+ renpho-py is UNOFFICIAL and is not affiliated with, endorsed by, or supported by
14
+ Renpho or any of its affiliates. "Renpho" is a trademark of its respective
15
+ owner and is used here only to describe the API this client targets. Use at
16
+ your own risk and in accordance with Renpho's terms of service.
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: renpho-py
3
+ Version: 1.0.0
4
+ Summary: Unofficial Renpho Health API client for Python — pull body composition data from Renpho smart scales.
5
+ Author: ChocoTonic
6
+ Maintainer: ChocoTonic
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/ChocoTonic/renpho-py
9
+ Project-URL: Repository, https://github.com/ChocoTonic/renpho-py
10
+ Project-URL: Issues, https://github.com/ChocoTonic/renpho-py/issues
11
+ Project-URL: Changelog, https://github.com/ChocoTonic/renpho-py/blob/main/CHANGELOG.md
12
+ Keywords: renpho,renpho-api,health,body-composition,smart-scale,api-client,python
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ License-File: NOTICE
26
+ Requires-Dist: requests>=2.28
27
+ Requires-Dist: pycryptodome>=3.15
28
+ Provides-Extra: dotenv
29
+ Requires-Dist: python-dotenv>=1.0; extra == "dotenv"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # renpho-py — Renpho Health API client for Python
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/renpho-py)](https://pypi.org/project/renpho-py/)
37
+ [![CI](https://github.com/ChocoTonic/renpho-py/actions/workflows/ci.yml/badge.svg)](https://github.com/ChocoTonic/renpho-py/actions/workflows/ci.yml)
38
+ [![Python](https://img.shields.io/pypi/pyversions/renpho-py)](https://pypi.org/project/renpho-py/)
39
+
40
+ Unofficial **Renpho Health API** client for **Python**. Pull body composition
41
+ measurements from Renpho smart scales programmatically.
42
+
43
+ > **Unofficial.** Not affiliated with, endorsed by, or supported by Renpho. Use
44
+ > at your own risk and in line with Renpho's terms of service.
45
+
46
+ `renpho-py` is an independently maintained continuation of the abandoned
47
+ [`renpho-api`](https://github.com/danvaneijck/renpho-api) (MIT). The import name
48
+ is unchanged, so migrating is a one-line swap — `pip install renpho-py` and your
49
+ existing `from renpho import ...` code keeps working. The underlying API was
50
+ reverse-engineered; protocol details are based on
51
+ [RenphoGarminSync-CLI](https://github.com/forkerer/RenphoGarminSync-CLI).
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install renpho-py
57
+ ```
58
+
59
+ For `.env` file support (recommended for CLI usage):
60
+
61
+ ```bash
62
+ pip install "renpho-py[dotenv]"
63
+ ```
64
+
65
+ > Migrating from `renpho-api`? `pip uninstall renpho-api && pip install renpho-py`.
66
+ > No code changes — you still `from renpho import RenphoClient`.
67
+
68
+ ## CLI Usage
69
+
70
+ 1. Create a `.env` file (or export the variables):
71
+
72
+ ```
73
+ RENPHO_EMAIL=your@email.com
74
+ RENPHO_PASSWORD=your_plain_text_password
75
+ ```
76
+
77
+ 2. Run the CLI:
78
+
79
+ ```bash
80
+ renpho
81
+ ```
82
+
83
+ This will log in, discover your scales, fetch all measurements, print the 5 most recent, and save everything to `renpho_data/` as JSON and CSV.
84
+
85
+ ### Environment variables
86
+
87
+ | Variable | Required | Description |
88
+ | --- | --- | --- |
89
+ | `RENPHO_EMAIL` | Yes | Your Renpho account email |
90
+ | `RENPHO_PASSWORD` | Yes | Your Renpho account password |
91
+ | `RENPHO_DEBUG` | No | Set to `1` to print API request/response details |
92
+ | `RENPHO_OUTPUT_DIR` | No | Output directory (default: `renpho_data`) |
93
+
94
+ ## Library Usage
95
+
96
+ ```python
97
+ from renpho import RenphoClient
98
+
99
+ client = RenphoClient("user@example.com", "password")
100
+ client.login()
101
+
102
+ # Fetch all measurements in one call
103
+ measurements = client.get_all_measurements()
104
+
105
+ for m in measurements:
106
+ print(m["weight"], m.get("bodyfat"), m.get("muscle"))
107
+ ```
108
+
109
+ ### Step-by-step control
110
+
111
+ ```python
112
+ from renpho import RenphoClient, save_json, save_csv
113
+
114
+ client = RenphoClient("user@example.com", "password")
115
+ client.login()
116
+
117
+ # Get device/scale info
118
+ device_info = client.get_device_info()
119
+ scales = device_info["scale"]
120
+
121
+ # Fetch from a specific scale table
122
+ # Use get_body_composition_measurements() for scales with impedance sensors
123
+ # (body fat, muscle, etc.) — the server-side count is unreliable for these.
124
+ # Fall back to get_measurements() for weight-only scales.
125
+ table = scales[0]
126
+ measurements = client.get_body_composition_measurements(
127
+ table_name=table["tableName"],
128
+ user_id=client.user_id,
129
+ )
130
+ if not measurements:
131
+ measurements = client.get_measurements(
132
+ table_name=table["tableName"],
133
+ user_id=client.user_id,
134
+ total_count=table["count"],
135
+ )
136
+
137
+ # Export
138
+ save_json(measurements, "my_data.json")
139
+ save_csv(measurements, "my_data.csv")
140
+ ```
141
+
142
+ ### Multiple Renpho accounts on one email
143
+
144
+ Some users end up with **two Renpho accounts under the same email** — for
145
+ example after the Google SSO migration created an orphan account, or after
146
+ re-registering. Each account has its own user ID and its own measurement
147
+ table, so the default `get_all_measurements()` will only return data from
148
+ the account you log in to.
149
+
150
+ If you know the other account's user ID, pass it in:
151
+
152
+ ```python
153
+ measurements = client.get_all_measurements(
154
+ extra_user_ids=["5975813831868809088"],
155
+ )
156
+ ```
157
+
158
+ The library will probe every measurement table for that user ID, fetch
159
+ matching records, and dedupe by record `id` so you get a single combined
160
+ timeline.
161
+
162
+ **How to find your other user ID:**
163
+
164
+ Unfortunately there is no first-party API endpoint that lists "all
165
+ accounts associated with this email" — Renpho treats accounts as
166
+ independent even when emails collide. Options:
167
+
168
+ 1. **Renpho support** — email them and ask for your user ID(s) on file
169
+ 2. **Inspect the iOS / Android app** — sign in to the other account in
170
+ the official app and look in Settings / Account / Help → Feedback
171
+ pages (the user ID is sometimes visible there)
172
+ 3. **Capture network traffic** — proxy the official app through
173
+ mitmproxy, sign in, and look at any request body containing
174
+ `userId` (decrypt with the published AES-128 key — see the
175
+ reverse-engineering write-up linked at the top of this README)
176
+
177
+ Once you have the ID, save it alongside your credentials and you won't
178
+ need to discover it again.
179
+
180
+ ### Error handling
181
+
182
+ ```python
183
+ from renpho import RenphoClient, RenphoAPIError
184
+
185
+ client = RenphoClient("user@example.com", "wrong_password")
186
+ try:
187
+ client.login()
188
+ except RenphoAPIError as e:
189
+ print(f"API error: {e}")
190
+ ```
191
+
192
+ ## Available Metrics
193
+
194
+ Each measurement dict can contain these fields (availability depends on your scale model):
195
+
196
+ | Key | Description | Unit |
197
+ | --- | --- | --- |
198
+ | `weight` | Weight | kg |
199
+ | `bmi` | BMI | |
200
+ | `bodyfat` | Body Fat | % |
201
+ | `water` | Body Water | % |
202
+ | `muscle` | Muscle Mass | % |
203
+ | `bone` | Bone Mass | % |
204
+ | `bmr` | Basal Metabolic Rate | kcal/day |
205
+ | `visfat` | Visceral Fat | level |
206
+ | `subfat` | Subcutaneous Fat | % |
207
+ | `protein` | Protein | % |
208
+ | `bodyage` | Body Age | years |
209
+ | `sinew` | Lean Body Mass | kg |
210
+ | `fatFreeWeight` | Fat Free Weight | kg |
211
+ | `heartRate` | Heart Rate | bpm |
212
+ | `cardiacIndex` | Cardiac Index | |
213
+ | `bodyShape` | Body Shape | |
214
+
215
+ ## Project Structure
216
+
217
+ ```
218
+ renpho-py/
219
+ ├── pyproject.toml # Package config & dependencies (dist name: renpho-py)
220
+ ├── README.md
221
+ ├── CHANGELOG.md
222
+ ├── LICENSE # MIT (original + current attribution)
223
+ ├── NOTICE
224
+ ├── renpho/ # import name — unchanged for drop-in migration
225
+ │ ├── __init__.py # Public API exports
226
+ │ ├── py.typed # PEP 561 typing marker
227
+ │ ├── client.py # RenphoClient class
228
+ │ ├── cli.py # CLI entry point
229
+ │ ├── constants.py # API endpoints, device types, metrics
230
+ │ ├── crypto.py # AES encryption/decryption
231
+ │ └── export.py # JSON/CSV export helpers
232
+ ├── tests/ # Unit tests
233
+ └── .github/workflows/ # CI + PyPI release automation (trusted publishing)
234
+ ```
@@ -0,0 +1,201 @@
1
+ # renpho-py — Renpho Health API client for Python
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/renpho-py)](https://pypi.org/project/renpho-py/)
4
+ [![CI](https://github.com/ChocoTonic/renpho-py/actions/workflows/ci.yml/badge.svg)](https://github.com/ChocoTonic/renpho-py/actions/workflows/ci.yml)
5
+ [![Python](https://img.shields.io/pypi/pyversions/renpho-py)](https://pypi.org/project/renpho-py/)
6
+
7
+ Unofficial **Renpho Health API** client for **Python**. Pull body composition
8
+ measurements from Renpho smart scales programmatically.
9
+
10
+ > **Unofficial.** Not affiliated with, endorsed by, or supported by Renpho. Use
11
+ > at your own risk and in line with Renpho's terms of service.
12
+
13
+ `renpho-py` is an independently maintained continuation of the abandoned
14
+ [`renpho-api`](https://github.com/danvaneijck/renpho-api) (MIT). The import name
15
+ is unchanged, so migrating is a one-line swap — `pip install renpho-py` and your
16
+ existing `from renpho import ...` code keeps working. The underlying API was
17
+ reverse-engineered; protocol details are based on
18
+ [RenphoGarminSync-CLI](https://github.com/forkerer/RenphoGarminSync-CLI).
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install renpho-py
24
+ ```
25
+
26
+ For `.env` file support (recommended for CLI usage):
27
+
28
+ ```bash
29
+ pip install "renpho-py[dotenv]"
30
+ ```
31
+
32
+ > Migrating from `renpho-api`? `pip uninstall renpho-api && pip install renpho-py`.
33
+ > No code changes — you still `from renpho import RenphoClient`.
34
+
35
+ ## CLI Usage
36
+
37
+ 1. Create a `.env` file (or export the variables):
38
+
39
+ ```
40
+ RENPHO_EMAIL=your@email.com
41
+ RENPHO_PASSWORD=your_plain_text_password
42
+ ```
43
+
44
+ 2. Run the CLI:
45
+
46
+ ```bash
47
+ renpho
48
+ ```
49
+
50
+ This will log in, discover your scales, fetch all measurements, print the 5 most recent, and save everything to `renpho_data/` as JSON and CSV.
51
+
52
+ ### Environment variables
53
+
54
+ | Variable | Required | Description |
55
+ | --- | --- | --- |
56
+ | `RENPHO_EMAIL` | Yes | Your Renpho account email |
57
+ | `RENPHO_PASSWORD` | Yes | Your Renpho account password |
58
+ | `RENPHO_DEBUG` | No | Set to `1` to print API request/response details |
59
+ | `RENPHO_OUTPUT_DIR` | No | Output directory (default: `renpho_data`) |
60
+
61
+ ## Library Usage
62
+
63
+ ```python
64
+ from renpho import RenphoClient
65
+
66
+ client = RenphoClient("user@example.com", "password")
67
+ client.login()
68
+
69
+ # Fetch all measurements in one call
70
+ measurements = client.get_all_measurements()
71
+
72
+ for m in measurements:
73
+ print(m["weight"], m.get("bodyfat"), m.get("muscle"))
74
+ ```
75
+
76
+ ### Step-by-step control
77
+
78
+ ```python
79
+ from renpho import RenphoClient, save_json, save_csv
80
+
81
+ client = RenphoClient("user@example.com", "password")
82
+ client.login()
83
+
84
+ # Get device/scale info
85
+ device_info = client.get_device_info()
86
+ scales = device_info["scale"]
87
+
88
+ # Fetch from a specific scale table
89
+ # Use get_body_composition_measurements() for scales with impedance sensors
90
+ # (body fat, muscle, etc.) — the server-side count is unreliable for these.
91
+ # Fall back to get_measurements() for weight-only scales.
92
+ table = scales[0]
93
+ measurements = client.get_body_composition_measurements(
94
+ table_name=table["tableName"],
95
+ user_id=client.user_id,
96
+ )
97
+ if not measurements:
98
+ measurements = client.get_measurements(
99
+ table_name=table["tableName"],
100
+ user_id=client.user_id,
101
+ total_count=table["count"],
102
+ )
103
+
104
+ # Export
105
+ save_json(measurements, "my_data.json")
106
+ save_csv(measurements, "my_data.csv")
107
+ ```
108
+
109
+ ### Multiple Renpho accounts on one email
110
+
111
+ Some users end up with **two Renpho accounts under the same email** — for
112
+ example after the Google SSO migration created an orphan account, or after
113
+ re-registering. Each account has its own user ID and its own measurement
114
+ table, so the default `get_all_measurements()` will only return data from
115
+ the account you log in to.
116
+
117
+ If you know the other account's user ID, pass it in:
118
+
119
+ ```python
120
+ measurements = client.get_all_measurements(
121
+ extra_user_ids=["5975813831868809088"],
122
+ )
123
+ ```
124
+
125
+ The library will probe every measurement table for that user ID, fetch
126
+ matching records, and dedupe by record `id` so you get a single combined
127
+ timeline.
128
+
129
+ **How to find your other user ID:**
130
+
131
+ Unfortunately there is no first-party API endpoint that lists "all
132
+ accounts associated with this email" — Renpho treats accounts as
133
+ independent even when emails collide. Options:
134
+
135
+ 1. **Renpho support** — email them and ask for your user ID(s) on file
136
+ 2. **Inspect the iOS / Android app** — sign in to the other account in
137
+ the official app and look in Settings / Account / Help → Feedback
138
+ pages (the user ID is sometimes visible there)
139
+ 3. **Capture network traffic** — proxy the official app through
140
+ mitmproxy, sign in, and look at any request body containing
141
+ `userId` (decrypt with the published AES-128 key — see the
142
+ reverse-engineering write-up linked at the top of this README)
143
+
144
+ Once you have the ID, save it alongside your credentials and you won't
145
+ need to discover it again.
146
+
147
+ ### Error handling
148
+
149
+ ```python
150
+ from renpho import RenphoClient, RenphoAPIError
151
+
152
+ client = RenphoClient("user@example.com", "wrong_password")
153
+ try:
154
+ client.login()
155
+ except RenphoAPIError as e:
156
+ print(f"API error: {e}")
157
+ ```
158
+
159
+ ## Available Metrics
160
+
161
+ Each measurement dict can contain these fields (availability depends on your scale model):
162
+
163
+ | Key | Description | Unit |
164
+ | --- | --- | --- |
165
+ | `weight` | Weight | kg |
166
+ | `bmi` | BMI | |
167
+ | `bodyfat` | Body Fat | % |
168
+ | `water` | Body Water | % |
169
+ | `muscle` | Muscle Mass | % |
170
+ | `bone` | Bone Mass | % |
171
+ | `bmr` | Basal Metabolic Rate | kcal/day |
172
+ | `visfat` | Visceral Fat | level |
173
+ | `subfat` | Subcutaneous Fat | % |
174
+ | `protein` | Protein | % |
175
+ | `bodyage` | Body Age | years |
176
+ | `sinew` | Lean Body Mass | kg |
177
+ | `fatFreeWeight` | Fat Free Weight | kg |
178
+ | `heartRate` | Heart Rate | bpm |
179
+ | `cardiacIndex` | Cardiac Index | |
180
+ | `bodyShape` | Body Shape | |
181
+
182
+ ## Project Structure
183
+
184
+ ```
185
+ renpho-py/
186
+ ├── pyproject.toml # Package config & dependencies (dist name: renpho-py)
187
+ ├── README.md
188
+ ├── CHANGELOG.md
189
+ ├── LICENSE # MIT (original + current attribution)
190
+ ├── NOTICE
191
+ ├── renpho/ # import name — unchanged for drop-in migration
192
+ │ ├── __init__.py # Public API exports
193
+ │ ├── py.typed # PEP 561 typing marker
194
+ │ ├── client.py # RenphoClient class
195
+ │ ├── cli.py # CLI entry point
196
+ │ ├── constants.py # API endpoints, device types, metrics
197
+ │ ├── crypto.py # AES encryption/decryption
198
+ │ └── export.py # JSON/CSV export helpers
199
+ ├── tests/ # Unit tests
200
+ └── .github/workflows/ # CI + PyPI release automation (trusted publishing)
201
+ ```
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "renpho-py"
7
+ version = "1.0.0"
8
+ description = "Unofficial Renpho Health API client for Python — pull body composition data from Renpho smart scales."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE", "NOTICE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "ChocoTonic" }]
14
+ maintainers = [{ name = "ChocoTonic" }]
15
+ keywords = [
16
+ "renpho",
17
+ "renpho-api",
18
+ "health",
19
+ "body-composition",
20
+ "smart-scale",
21
+ "api-client",
22
+ "python",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Topic :: Software Development :: Libraries",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = [
36
+ "requests>=2.28",
37
+ "pycryptodome>=3.15",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dotenv = ["python-dotenv>=1.0"]
42
+ dev = ["pytest>=7.0"]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/ChocoTonic/renpho-py"
46
+ Repository = "https://github.com/ChocoTonic/renpho-py"
47
+ Issues = "https://github.com/ChocoTonic/renpho-py/issues"
48
+ Changelog = "https://github.com/ChocoTonic/renpho-py/blob/main/CHANGELOG.md"
49
+
50
+ [project.scripts]
51
+ renpho = "renpho.cli:main"
52
+
53
+ [tool.setuptools]
54
+ packages = ["renpho"]
55
+
56
+ [tool.setuptools.package-data]
57
+ renpho = ["py.typed"]
58
+
59
+ [dependency-groups]
60
+ dev = [
61
+ "pytest-cov>=7.1.0",
62
+ ]
@@ -0,0 +1,24 @@
1
+ """renpho - Unofficial Python client for the Renpho Health API.
2
+
3
+ Pull body composition measurements from Renpho smart scales.
4
+
5
+ Quick start::
6
+
7
+ from renpho import RenphoClient
8
+
9
+ client = RenphoClient("user@example.com", "password")
10
+ client.login()
11
+ measurements = client.get_all_measurements()
12
+ """
13
+
14
+ from .client import RenphoAPIError, RenphoClient
15
+ from .export import format_measurement, format_timestamp, save_csv, save_json
16
+
17
+ __all__ = [
18
+ "RenphoClient",
19
+ "RenphoAPIError",
20
+ "format_measurement",
21
+ "format_timestamp",
22
+ "save_csv",
23
+ "save_json",
24
+ ]
@@ -0,0 +1,111 @@
1
+ """Command-line interface for pulling Renpho scale data."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ try:
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+ except ImportError:
12
+ pass
13
+
14
+ from .client import RenphoClient
15
+ from .export import format_measurement, save_csv, save_json
16
+
17
+
18
+ def main(argv: list[str] | None = None) -> None:
19
+ """Entry point for the ``renpho`` CLI command."""
20
+ email = os.getenv("RENPHO_EMAIL", "")
21
+ password = os.getenv("RENPHO_PASSWORD", "")
22
+ debug = os.getenv("RENPHO_DEBUG", "").lower() in ("1", "true", "yes")
23
+ output_dir = Path(os.getenv("RENPHO_OUTPUT_DIR", "renpho_data"))
24
+
25
+ if not email or not password:
26
+ print("Missing credentials!\n")
27
+ print("Create a .env file with:")
28
+ print(" RENPHO_EMAIL=your@email.com")
29
+ print(" RENPHO_PASSWORD=your_plain_text_password")
30
+ print(" RENPHO_DEBUG=1 # optional")
31
+ sys.exit(1)
32
+
33
+ client = RenphoClient(email, password, debug=debug)
34
+
35
+ # Step 1: Login
36
+ print(f"Logging in as {email}...")
37
+ client.login()
38
+ print(f"Logged in! User ID: {client.user_id}")
39
+
40
+ # Step 2: Device info
41
+ print("Getting device info...")
42
+ device_info = client.get_device_info()
43
+
44
+ if not device_info:
45
+ print("Could not get device info")
46
+ sys.exit(1)
47
+
48
+ save_json(device_info, output_dir / "device_info.json")
49
+
50
+ scales = device_info.get("scale", [])
51
+ if not scales:
52
+ print("No scales found in device info")
53
+ print(f" Device info keys: {list(device_info.keys())}")
54
+ sys.exit(1)
55
+
56
+ print(f"\nFound {len(scales)} scale table(s):")
57
+ for i, scale in enumerate(scales):
58
+ table = scale.get("tableName", "unknown")
59
+ count = scale.get("count", 0)
60
+ uids = scale.get("userIds", [])
61
+ print(f" [{i}] table={table}, records={count}, users={uids}")
62
+
63
+ # Step 3: Fetch measurements
64
+ all_measurements: list[dict] = []
65
+ for scale in scales:
66
+ table_name = scale.get("tableName")
67
+ count = scale.get("count", 0)
68
+ user_ids = scale.get("userIds", [])
69
+
70
+ if not table_name or count == 0:
71
+ continue
72
+
73
+ uid = client.user_id
74
+ if user_ids and uid not in user_ids:
75
+ uid = user_ids[0]
76
+
77
+ print(f"Fetching measurements (table: {table_name}, total: {count})...")
78
+ measurements = client.get_measurements(table_name, uid, count)
79
+ all_measurements.extend(measurements)
80
+
81
+ if not all_measurements:
82
+ print("\nNo measurements found.")
83
+ print(" Try setting RENPHO_DEBUG=1 to see API responses.")
84
+ return
85
+
86
+ # Sort newest first
87
+ all_measurements.sort(
88
+ key=lambda m: m.get("timeStamp", 0) or 0,
89
+ reverse=True,
90
+ )
91
+
92
+ print(f"\nTotal: {len(all_measurements)} measurement(s)")
93
+
94
+ # Display most recent 5
95
+ for i, m in enumerate(all_measurements[:5]):
96
+ print(f"\n{'=' * 55}")
97
+ print(f" Measurement #{i + 1}")
98
+ print(f"{'=' * 55}")
99
+ print(format_measurement(m))
100
+
101
+ if len(all_measurements) > 5:
102
+ print(f"\n ... and {len(all_measurements) - 5} more")
103
+
104
+ # Save data
105
+ save_json(all_measurements, output_dir / "measurements.json")
106
+ save_csv(all_measurements, output_dir / "measurements.csv")
107
+
108
+ if client.user_info:
109
+ save_json(client.user_info, output_dir / "user_profile.json")
110
+
111
+ print(f"\nDone! Data saved to '{output_dir}/' folder.")