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.
- renpho_py-1.0.0/LICENSE +22 -0
- renpho_py-1.0.0/NOTICE +16 -0
- renpho_py-1.0.0/PKG-INFO +234 -0
- renpho_py-1.0.0/README.md +201 -0
- renpho_py-1.0.0/pyproject.toml +62 -0
- renpho_py-1.0.0/renpho/__init__.py +24 -0
- renpho_py-1.0.0/renpho/cli.py +111 -0
- renpho_py-1.0.0/renpho/client.py +398 -0
- renpho_py-1.0.0/renpho/constants.py +52 -0
- renpho_py-1.0.0/renpho/crypto.py +54 -0
- renpho_py-1.0.0/renpho/export.py +95 -0
- renpho_py-1.0.0/renpho/py.typed +0 -0
- renpho_py-1.0.0/renpho_py.egg-info/PKG-INFO +234 -0
- renpho_py-1.0.0/renpho_py.egg-info/SOURCES.txt +21 -0
- renpho_py-1.0.0/renpho_py.egg-info/dependency_links.txt +1 -0
- renpho_py-1.0.0/renpho_py.egg-info/entry_points.txt +2 -0
- renpho_py-1.0.0/renpho_py.egg-info/requires.txt +8 -0
- renpho_py-1.0.0/renpho_py.egg-info/top_level.txt +1 -0
- renpho_py-1.0.0/setup.cfg +4 -0
- renpho_py-1.0.0/tests/test_client.py +261 -0
- renpho_py-1.0.0/tests/test_constants.py +40 -0
- renpho_py-1.0.0/tests/test_crypto.py +54 -0
- renpho_py-1.0.0/tests/test_export.py +73 -0
renpho_py-1.0.0/LICENSE
ADDED
|
@@ -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.
|
renpho_py-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/renpho-py/)
|
|
37
|
+
[](https://github.com/ChocoTonic/renpho-py/actions/workflows/ci.yml)
|
|
38
|
+
[](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
|
+
[](https://pypi.org/project/renpho-py/)
|
|
4
|
+
[](https://github.com/ChocoTonic/renpho-py/actions/workflows/ci.yml)
|
|
5
|
+
[](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.")
|