socketry 0.1.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.
- socketry-0.1.0/PKG-INFO +213 -0
- socketry-0.1.0/README.md +199 -0
- socketry-0.1.0/pyproject.toml +115 -0
- socketry-0.1.0/src/socketry/__init__.py +6 -0
- socketry-0.1.0/src/socketry/__main__.py +5 -0
- socketry-0.1.0/src/socketry/_constants.py +52 -0
- socketry-0.1.0/src/socketry/_crypto.py +51 -0
- socketry-0.1.0/src/socketry/cli.py +275 -0
- socketry-0.1.0/src/socketry/client.py +520 -0
- socketry-0.1.0/src/socketry/properties.py +161 -0
- socketry-0.1.0/src/socketry/py.typed +0 -0
socketry-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: socketry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python API and CLI for controlling Jackery portable power stations
|
|
5
|
+
Author: Jesus Lopez
|
|
6
|
+
Author-email: Jesus Lopez <jesus@jesusla.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: typer>=0.9
|
|
9
|
+
Requires-Dist: paho-mqtt>=2.0
|
|
10
|
+
Requires-Dist: requests>=2.31
|
|
11
|
+
Requires-Dist: pycryptodome>=3.19
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# socketry
|
|
16
|
+
|
|
17
|
+
[](https://github.com/jlopez/socketry/actions/workflows/ci.yml)
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
Python API and CLI for controlling Jackery portable power stations.
|
|
21
|
+
|
|
22
|
+
Reverse-engineered from the Jackery Android APK (v1.0.7) and iOS app (v1.2.0).
|
|
23
|
+
Communicates via Jackery's cloud MQTT broker and HTTP API — no modifications to
|
|
24
|
+
the device or its firmware.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx --from git+https://github.com/jlopez/socketry socketry login --email you@example.com --password 'yourpass'
|
|
30
|
+
uvx --from git+https://github.com/jlopez/socketry socketry get
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install it once and use `socketry` directly:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install git+https://github.com/jlopez/socketry
|
|
37
|
+
socketry login --email you@example.com --password 'yourpass'
|
|
38
|
+
socketry get
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Supported devices
|
|
42
|
+
|
|
43
|
+
All 10 models in the current Jackery app share the same protocol:
|
|
44
|
+
|
|
45
|
+
| Model | Code |
|
|
46
|
+
|-------|------|
|
|
47
|
+
| Explorer 3000 Pro | 1 |
|
|
48
|
+
| Explorer 2000 Plus | 2 |
|
|
49
|
+
| Explorer 300 Plus | 4 |
|
|
50
|
+
| Explorer 1000 Plus | 5 |
|
|
51
|
+
| Explorer 700 Plus | 6 |
|
|
52
|
+
| Explorer 280 Plus | 7 |
|
|
53
|
+
| Explorer 1000 Pro2 | 8 |
|
|
54
|
+
| Explorer 600 Plus | 9 |
|
|
55
|
+
| Explorer 240 | 10 |
|
|
56
|
+
| Explorer 2000 | 12 |
|
|
57
|
+
|
|
58
|
+
Properties and MQTT action IDs are exhaustive for this APK version. Unknown
|
|
59
|
+
properties returned by newer firmware are displayed as raw key/value pairs.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Install as a CLI tool (from GitHub)
|
|
65
|
+
uv tool install git+https://github.com/jlopez/socketry
|
|
66
|
+
|
|
67
|
+
# Or from PyPI (once published)
|
|
68
|
+
uv tool install socketry
|
|
69
|
+
|
|
70
|
+
# Or install as a library
|
|
71
|
+
pip install socketry
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## CLI usage
|
|
75
|
+
|
|
76
|
+
### Login
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Authenticates and discovers all devices (owned + shared with you)
|
|
80
|
+
socketry login --email you@example.com --password 'yourpass'
|
|
81
|
+
|
|
82
|
+
# List devices and select the active one
|
|
83
|
+
socketry devices
|
|
84
|
+
socketry select 0
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Credentials are saved to `~/.config/socketry/credentials.json` (mode 0600).
|
|
88
|
+
|
|
89
|
+
### Reading properties (`get`)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# All properties (colored + grouped on a TTY)
|
|
93
|
+
socketry get
|
|
94
|
+
|
|
95
|
+
# Single property — by CLI name or raw protocol key
|
|
96
|
+
socketry get battery # Battery: 85%
|
|
97
|
+
socketry get rb # Battery: 85% (same thing)
|
|
98
|
+
socketry get ac # AC output: ON
|
|
99
|
+
|
|
100
|
+
# JSON output (indented on TTY, compact when piped)
|
|
101
|
+
socketry get --json
|
|
102
|
+
socketry get ac --json # {"oac": 1}
|
|
103
|
+
socketry get --json | jq .rb # pipe-friendly
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Available properties:
|
|
107
|
+
|
|
108
|
+
| Group | Names |
|
|
109
|
+
|-------|-------|
|
|
110
|
+
| Battery & Power | `battery`, `battery-temp`, `battery-state`, `input-power`, `output-power`, `input-time`, `output-time` |
|
|
111
|
+
| I/O State | `ac`, `dc`, `usb`, `car`, `ac-in`, `dc-in`, `light`, `wireless` |
|
|
112
|
+
| Settings | `charge-speed`, `auto-shutdown`, `energy-saving`, `battery-protection`, `sfc`, `ups`, `screen-timeout` |
|
|
113
|
+
| AC / Power Detail | `ac-input-power`, `car-input-power`, `ac-voltage`, `ac-freq`, `ac-power`, `ac-power-2`, `ac-socket-power` |
|
|
114
|
+
| Other / Alarms | `error-code`, `temp-alarm`, `power-alarm`, `power-mode-battery`, `total-temp`, `system-status`, `power-capacity` |
|
|
115
|
+
|
|
116
|
+
Raw protocol keys (`rb`, `oac`, `bt`, ...) are also accepted.
|
|
117
|
+
|
|
118
|
+
### Changing settings (`set`)
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# I/O toggles
|
|
122
|
+
socketry set ac on
|
|
123
|
+
socketry set dc off
|
|
124
|
+
socketry set usb on
|
|
125
|
+
socketry set car off
|
|
126
|
+
|
|
127
|
+
# Light
|
|
128
|
+
socketry set light high # off | low | high | sos
|
|
129
|
+
|
|
130
|
+
# Device settings
|
|
131
|
+
socketry set charge-speed mute # fast | mute
|
|
132
|
+
socketry set battery-protection eco # full | eco
|
|
133
|
+
socketry set ups on
|
|
134
|
+
socketry set sfc on
|
|
135
|
+
|
|
136
|
+
# Integer settings
|
|
137
|
+
socketry set screen-timeout 30
|
|
138
|
+
socketry set auto-shutdown 60
|
|
139
|
+
socketry set energy-saving 30
|
|
140
|
+
|
|
141
|
+
# Wait for device confirmation
|
|
142
|
+
socketry set ac on --wait
|
|
143
|
+
|
|
144
|
+
# Show available settings
|
|
145
|
+
socketry set
|
|
146
|
+
socketry set light # "expects a value: off | low | high | sos"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Writable settings:
|
|
150
|
+
|
|
151
|
+
| Setting | Values | Description |
|
|
152
|
+
|---------|--------|-------------|
|
|
153
|
+
| `ac` | on / off | AC output |
|
|
154
|
+
| `dc` | on / off | DC output |
|
|
155
|
+
| `usb` | on / off | USB output |
|
|
156
|
+
| `car` | on / off | Car (12V) output |
|
|
157
|
+
| `ac-in` | on / off | AC input |
|
|
158
|
+
| `dc-in` | on / off | DC input |
|
|
159
|
+
| `light` | off / low / high / sos | Light mode |
|
|
160
|
+
| `screen-timeout` | integer | Screen timeout |
|
|
161
|
+
| `auto-shutdown` | integer | Auto shutdown timer |
|
|
162
|
+
| `charge-speed` | fast / mute | Charge speed mode |
|
|
163
|
+
| `battery-protection` | full / eco | Battery protection level |
|
|
164
|
+
| `energy-saving` | integer | Energy saving timeout |
|
|
165
|
+
| `sfc` | on / off | Super fast charge |
|
|
166
|
+
| `ups` | on / off | UPS mode |
|
|
167
|
+
|
|
168
|
+
## Library usage
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from socketry import Client
|
|
172
|
+
|
|
173
|
+
# Authenticate (or load saved credentials)
|
|
174
|
+
client = Client.login("email@example.com", "password")
|
|
175
|
+
client.save_credentials()
|
|
176
|
+
|
|
177
|
+
# Or load previously saved credentials
|
|
178
|
+
client = Client.from_saved()
|
|
179
|
+
|
|
180
|
+
# List and select devices
|
|
181
|
+
devices = client.fetch_devices()
|
|
182
|
+
client.select_device(0)
|
|
183
|
+
|
|
184
|
+
# Read properties
|
|
185
|
+
props = client.get_all_properties()
|
|
186
|
+
setting, value = client.get_property("battery")
|
|
187
|
+
print(f"{setting.name}: {setting.format_value(value)}")
|
|
188
|
+
|
|
189
|
+
# Control
|
|
190
|
+
client.set_property("ac", "on")
|
|
191
|
+
result = client.set_property("light", "high", wait=True)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## How it works
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
socketry ──HTTP──> iot.jackeryapp.com (login, device list, properties)
|
|
198
|
+
socketry ──MQTT──> emqx.jackeryapp.com (device control via encrypted TLS)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Login uses AES-192/ECB + RSA-1024 encrypted HTTP POST. Device control commands
|
|
202
|
+
are published over MQTT (TLS 1.2 with a self-signed CA). Status polling uses the
|
|
203
|
+
HTTP property endpoint. See [docs/protocol.md](docs/protocol.md) for the full
|
|
204
|
+
protocol specification.
|
|
205
|
+
|
|
206
|
+
## Roadmap
|
|
207
|
+
|
|
208
|
+
- [ ] MQTT real-time monitor (subscribe to live property changes)
|
|
209
|
+
- [ ] Token auto-refresh (JWT expires ~30 days)
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
socketry-0.1.0/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# socketry
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jlopez/socketry/actions/workflows/ci.yml)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Python API and CLI for controlling Jackery portable power stations.
|
|
7
|
+
|
|
8
|
+
Reverse-engineered from the Jackery Android APK (v1.0.7) and iOS app (v1.2.0).
|
|
9
|
+
Communicates via Jackery's cloud MQTT broker and HTTP API — no modifications to
|
|
10
|
+
the device or its firmware.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uvx --from git+https://github.com/jlopez/socketry socketry login --email you@example.com --password 'yourpass'
|
|
16
|
+
uvx --from git+https://github.com/jlopez/socketry socketry get
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it once and use `socketry` directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install git+https://github.com/jlopez/socketry
|
|
23
|
+
socketry login --email you@example.com --password 'yourpass'
|
|
24
|
+
socketry get
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Supported devices
|
|
28
|
+
|
|
29
|
+
All 10 models in the current Jackery app share the same protocol:
|
|
30
|
+
|
|
31
|
+
| Model | Code |
|
|
32
|
+
|-------|------|
|
|
33
|
+
| Explorer 3000 Pro | 1 |
|
|
34
|
+
| Explorer 2000 Plus | 2 |
|
|
35
|
+
| Explorer 300 Plus | 4 |
|
|
36
|
+
| Explorer 1000 Plus | 5 |
|
|
37
|
+
| Explorer 700 Plus | 6 |
|
|
38
|
+
| Explorer 280 Plus | 7 |
|
|
39
|
+
| Explorer 1000 Pro2 | 8 |
|
|
40
|
+
| Explorer 600 Plus | 9 |
|
|
41
|
+
| Explorer 240 | 10 |
|
|
42
|
+
| Explorer 2000 | 12 |
|
|
43
|
+
|
|
44
|
+
Properties and MQTT action IDs are exhaustive for this APK version. Unknown
|
|
45
|
+
properties returned by newer firmware are displayed as raw key/value pairs.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Install as a CLI tool (from GitHub)
|
|
51
|
+
uv tool install git+https://github.com/jlopez/socketry
|
|
52
|
+
|
|
53
|
+
# Or from PyPI (once published)
|
|
54
|
+
uv tool install socketry
|
|
55
|
+
|
|
56
|
+
# Or install as a library
|
|
57
|
+
pip install socketry
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI usage
|
|
61
|
+
|
|
62
|
+
### Login
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Authenticates and discovers all devices (owned + shared with you)
|
|
66
|
+
socketry login --email you@example.com --password 'yourpass'
|
|
67
|
+
|
|
68
|
+
# List devices and select the active one
|
|
69
|
+
socketry devices
|
|
70
|
+
socketry select 0
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Credentials are saved to `~/.config/socketry/credentials.json` (mode 0600).
|
|
74
|
+
|
|
75
|
+
### Reading properties (`get`)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# All properties (colored + grouped on a TTY)
|
|
79
|
+
socketry get
|
|
80
|
+
|
|
81
|
+
# Single property — by CLI name or raw protocol key
|
|
82
|
+
socketry get battery # Battery: 85%
|
|
83
|
+
socketry get rb # Battery: 85% (same thing)
|
|
84
|
+
socketry get ac # AC output: ON
|
|
85
|
+
|
|
86
|
+
# JSON output (indented on TTY, compact when piped)
|
|
87
|
+
socketry get --json
|
|
88
|
+
socketry get ac --json # {"oac": 1}
|
|
89
|
+
socketry get --json | jq .rb # pipe-friendly
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Available properties:
|
|
93
|
+
|
|
94
|
+
| Group | Names |
|
|
95
|
+
|-------|-------|
|
|
96
|
+
| Battery & Power | `battery`, `battery-temp`, `battery-state`, `input-power`, `output-power`, `input-time`, `output-time` |
|
|
97
|
+
| I/O State | `ac`, `dc`, `usb`, `car`, `ac-in`, `dc-in`, `light`, `wireless` |
|
|
98
|
+
| Settings | `charge-speed`, `auto-shutdown`, `energy-saving`, `battery-protection`, `sfc`, `ups`, `screen-timeout` |
|
|
99
|
+
| AC / Power Detail | `ac-input-power`, `car-input-power`, `ac-voltage`, `ac-freq`, `ac-power`, `ac-power-2`, `ac-socket-power` |
|
|
100
|
+
| Other / Alarms | `error-code`, `temp-alarm`, `power-alarm`, `power-mode-battery`, `total-temp`, `system-status`, `power-capacity` |
|
|
101
|
+
|
|
102
|
+
Raw protocol keys (`rb`, `oac`, `bt`, ...) are also accepted.
|
|
103
|
+
|
|
104
|
+
### Changing settings (`set`)
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# I/O toggles
|
|
108
|
+
socketry set ac on
|
|
109
|
+
socketry set dc off
|
|
110
|
+
socketry set usb on
|
|
111
|
+
socketry set car off
|
|
112
|
+
|
|
113
|
+
# Light
|
|
114
|
+
socketry set light high # off | low | high | sos
|
|
115
|
+
|
|
116
|
+
# Device settings
|
|
117
|
+
socketry set charge-speed mute # fast | mute
|
|
118
|
+
socketry set battery-protection eco # full | eco
|
|
119
|
+
socketry set ups on
|
|
120
|
+
socketry set sfc on
|
|
121
|
+
|
|
122
|
+
# Integer settings
|
|
123
|
+
socketry set screen-timeout 30
|
|
124
|
+
socketry set auto-shutdown 60
|
|
125
|
+
socketry set energy-saving 30
|
|
126
|
+
|
|
127
|
+
# Wait for device confirmation
|
|
128
|
+
socketry set ac on --wait
|
|
129
|
+
|
|
130
|
+
# Show available settings
|
|
131
|
+
socketry set
|
|
132
|
+
socketry set light # "expects a value: off | low | high | sos"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Writable settings:
|
|
136
|
+
|
|
137
|
+
| Setting | Values | Description |
|
|
138
|
+
|---------|--------|-------------|
|
|
139
|
+
| `ac` | on / off | AC output |
|
|
140
|
+
| `dc` | on / off | DC output |
|
|
141
|
+
| `usb` | on / off | USB output |
|
|
142
|
+
| `car` | on / off | Car (12V) output |
|
|
143
|
+
| `ac-in` | on / off | AC input |
|
|
144
|
+
| `dc-in` | on / off | DC input |
|
|
145
|
+
| `light` | off / low / high / sos | Light mode |
|
|
146
|
+
| `screen-timeout` | integer | Screen timeout |
|
|
147
|
+
| `auto-shutdown` | integer | Auto shutdown timer |
|
|
148
|
+
| `charge-speed` | fast / mute | Charge speed mode |
|
|
149
|
+
| `battery-protection` | full / eco | Battery protection level |
|
|
150
|
+
| `energy-saving` | integer | Energy saving timeout |
|
|
151
|
+
| `sfc` | on / off | Super fast charge |
|
|
152
|
+
| `ups` | on / off | UPS mode |
|
|
153
|
+
|
|
154
|
+
## Library usage
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from socketry import Client
|
|
158
|
+
|
|
159
|
+
# Authenticate (or load saved credentials)
|
|
160
|
+
client = Client.login("email@example.com", "password")
|
|
161
|
+
client.save_credentials()
|
|
162
|
+
|
|
163
|
+
# Or load previously saved credentials
|
|
164
|
+
client = Client.from_saved()
|
|
165
|
+
|
|
166
|
+
# List and select devices
|
|
167
|
+
devices = client.fetch_devices()
|
|
168
|
+
client.select_device(0)
|
|
169
|
+
|
|
170
|
+
# Read properties
|
|
171
|
+
props = client.get_all_properties()
|
|
172
|
+
setting, value = client.get_property("battery")
|
|
173
|
+
print(f"{setting.name}: {setting.format_value(value)}")
|
|
174
|
+
|
|
175
|
+
# Control
|
|
176
|
+
client.set_property("ac", "on")
|
|
177
|
+
result = client.set_property("light", "high", wait=True)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## How it works
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
socketry ──HTTP──> iot.jackeryapp.com (login, device list, properties)
|
|
184
|
+
socketry ──MQTT──> emqx.jackeryapp.com (device control via encrypted TLS)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Login uses AES-192/ECB + RSA-1024 encrypted HTTP POST. Device control commands
|
|
188
|
+
are published over MQTT (TLS 1.2 with a self-signed CA). Status polling uses the
|
|
189
|
+
HTTP property endpoint. See [docs/protocol.md](docs/protocol.md) for the full
|
|
190
|
+
protocol specification.
|
|
191
|
+
|
|
192
|
+
## Roadmap
|
|
193
|
+
|
|
194
|
+
- [ ] MQTT real-time monitor (subscribe to live property changes)
|
|
195
|
+
- [ ] Token auto-refresh (JWT expires ~30 days)
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "socketry"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python API and CLI for controlling Jackery portable power stations"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Jesus Lopez", email = "jesus@jesusla.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.9",
|
|
13
|
+
"paho-mqtt>=2.0",
|
|
14
|
+
"requests>=2.31",
|
|
15
|
+
"pycryptodome>=3.19",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
socketry = "socketry.cli:app"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.9.15,<0.10.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"mypy>=1.19.1",
|
|
28
|
+
"pre-commit>=4.5.1",
|
|
29
|
+
"pytest>=9.0.2",
|
|
30
|
+
"pytest-cov>=6.0.0",
|
|
31
|
+
"ruff>=0.14.14",
|
|
32
|
+
"types-requests>=2.31.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 100
|
|
37
|
+
target-version = "py311"
|
|
38
|
+
exclude = [
|
|
39
|
+
".git",
|
|
40
|
+
".mypy_cache",
|
|
41
|
+
".pytest_cache",
|
|
42
|
+
".ruff_cache",
|
|
43
|
+
".venv",
|
|
44
|
+
"venv",
|
|
45
|
+
"__pycache__",
|
|
46
|
+
"build",
|
|
47
|
+
"dist",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = [
|
|
52
|
+
"E", # pycodestyle errors
|
|
53
|
+
"W", # pycodestyle warnings
|
|
54
|
+
"F", # pyflakes
|
|
55
|
+
"I", # isort
|
|
56
|
+
"B", # flake8-bugbear
|
|
57
|
+
"C4", # flake8-comprehensions
|
|
58
|
+
"SIM", # flake8-simplify
|
|
59
|
+
"UP", # pyupgrade
|
|
60
|
+
]
|
|
61
|
+
ignore = []
|
|
62
|
+
fixable = ["ALL"]
|
|
63
|
+
unfixable = []
|
|
64
|
+
|
|
65
|
+
[tool.ruff.format]
|
|
66
|
+
quote-style = "double"
|
|
67
|
+
indent-style = "space"
|
|
68
|
+
line-ending = "auto"
|
|
69
|
+
|
|
70
|
+
[tool.mypy]
|
|
71
|
+
python_version = "3.11"
|
|
72
|
+
strict = true
|
|
73
|
+
warn_return_any = true
|
|
74
|
+
warn_unused_configs = true
|
|
75
|
+
disallow_untyped_defs = true
|
|
76
|
+
disallow_incomplete_defs = true
|
|
77
|
+
check_untyped_defs = true
|
|
78
|
+
no_implicit_optional = true
|
|
79
|
+
warn_redundant_casts = true
|
|
80
|
+
warn_unused_ignores = true
|
|
81
|
+
warn_no_return = true
|
|
82
|
+
ignore_missing_imports = false
|
|
83
|
+
disable_error_code = ["import-untyped"]
|
|
84
|
+
|
|
85
|
+
[[tool.mypy.overrides]]
|
|
86
|
+
module = ["tests.*"]
|
|
87
|
+
disallow_untyped_defs = false
|
|
88
|
+
|
|
89
|
+
[tool.pytest.ini_options]
|
|
90
|
+
testpaths = ["tests"]
|
|
91
|
+
python_files = ["test_*.py"]
|
|
92
|
+
python_classes = ["Test*"]
|
|
93
|
+
python_functions = ["test_*"]
|
|
94
|
+
addopts = [
|
|
95
|
+
"-v",
|
|
96
|
+
"--cov=socketry",
|
|
97
|
+
"--cov-report=term-missing",
|
|
98
|
+
"--cov-report=html",
|
|
99
|
+
"--cov-report=json",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[tool.coverage.run]
|
|
103
|
+
source = ["src/socketry"]
|
|
104
|
+
omit = ["tests/*", "**/__pycache__/*"]
|
|
105
|
+
|
|
106
|
+
[tool.coverage.report]
|
|
107
|
+
fail_under = 20
|
|
108
|
+
exclude_lines = [
|
|
109
|
+
"pragma: no cover",
|
|
110
|
+
"def __repr__",
|
|
111
|
+
"raise AssertionError",
|
|
112
|
+
"raise NotImplementedError",
|
|
113
|
+
"if __name__ == .__main__.:",
|
|
114
|
+
"if TYPE_CHECKING:",
|
|
115
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Internal constants extracted from the decompiled Jackery APK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
API_BASE = "https://iot.jackeryapp.com/v1"
|
|
8
|
+
|
|
9
|
+
MQTT_HOST = "emqx.jackeryapp.com"
|
|
10
|
+
MQTT_PORT = 8883
|
|
11
|
+
|
|
12
|
+
RSA_PUBLIC_KEY_B64 = (
|
|
13
|
+
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVmzgJy/4XolxPnkfu32YtJqYG"
|
|
14
|
+
"FLYqf9/rnVgURJED+8J9J3Pccd6+9L97/+7COZE5OkejsgOkqeLNC9C3r5mhpE4z"
|
|
15
|
+
"k/HStss7Q8/5DqkGD1annQ+eoICo3oi0dITZ0Qll56Dowb8lXi6WHViVDdih/oeU"
|
|
16
|
+
"wVJY89uJNtTWrz7t7QIDAQAB"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Self-signed CA cert for emqx.jackeryapp.com (from APK res/raw/ca.crt)
|
|
20
|
+
CA_CERT_PEM = """\
|
|
21
|
+
-----BEGIN CERTIFICATE-----
|
|
22
|
+
MIIDtTCCAp2gAwIBAgIJAPvYSRLMmPACMA0GCSqGSIb3DQEBCwUAMHAxCzAJBgNV
|
|
23
|
+
BAYTAkNOMRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMRQw
|
|
24
|
+
EgYDVQQKDAtqYWNrZXJ5LmNvbTELMAkGA1UECwwCY2ExFzAVBgNVBAMMDmNhLmph
|
|
25
|
+
Y2tlcnkuY29tMCAXDTIyMTIyMzEwMTc0N1oYDzIwNzcwOTI1MTAxNzQ3WjBwMQsw
|
|
26
|
+
CQYDVQQGEwJDTjESMBAGA1UECAwJR3Vhbmdkb25nMREwDwYDVQQHDAhTaGVuemhl
|
|
27
|
+
bjEUMBIGA1UECgwLamFja2VyeS5jb20xCzAJBgNVBAsMAmNhMRcwFQYDVQQDDA5j
|
|
28
|
+
YS5qYWNrZXJ5LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOrf
|
|
29
|
+
QltVp+PDphQ20tfQbCh/YlqIAK8VkIcYXq7DlVsX1HGl5x+6UkEahzLRtZFaWRkH
|
|
30
|
+
HiHSvol8I+cvq6BHte0VsjKAzl7Mae7P/UyQXwpgNa+hliZHoEqflghzYvxjlZeP
|
|
31
|
+
eOGGcHxg1p2M8+PeNWkX5VkVTSYi/abDz86+D5y1gq7S8n+tYk1WhKFHvIrfX3nN
|
|
32
|
+
4QXfDO7vAQMd1uc6YdDqRanWjxIgOSDk9B+Mblz0TxCR+hnuDDQpAE4ONjByjArS
|
|
33
|
+
MC/QS8BIq/TL6nixzA8y0vOHySmuOLfuhFpNoO2mujhBGN/Dmq/pZwmsKSK91PxE
|
|
34
|
+
dn3YO8N8q7flHd/Qw4UCAwEAAaNQME4wHQYDVR0OBBYEFL/rQk0x4WclVgw3WLsl
|
|
35
|
+
YH3k0dvgMB8GA1UdIwQYMBaAFL/rQk0x4WclVgw3WLslYH3k0dvgMAwGA1UdEwQF
|
|
36
|
+
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALZM+xA4bUnO/7/0giZ3xUPEKzwFDp4G
|
|
37
|
+
5UPI/5grLYxp38t2M84tlJ94W/HKH+f1CYbJ6m28dSZfWtnRzQ3Tgq0whrsmYiK9
|
|
38
|
+
1Txcl3HPBiL7yAn3yE8DjHV+S2eSnN0o26/rcXCe+9bghSqqGaVDOJyk+Fm4l17e
|
|
39
|
+
Hzx99PvPGkpGUglun3UEp/Vp5ZUl9uDYT813HJ9jK80i1MDlzBJWmg7gzh27/Qls
|
|
40
|
+
UJLtYvgsxiBKAnK8YkAyu51Jm8uLz1BZ1RANf22vv0QUTW+SGdgc5Q1h610G9N1i
|
|
41
|
+
4BaijfWnto9ka32QKgZA0gHXsT3wiwdbEow0lp7y40aiXq4kazDT7ws=
|
|
42
|
+
-----END CERTIFICATE-----
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
CRED_DIR = Path.home() / ".config" / "socketry"
|
|
46
|
+
CRED_FILE = CRED_DIR / "credentials.json"
|
|
47
|
+
|
|
48
|
+
APP_HEADERS: dict[str, str] = {
|
|
49
|
+
"platform": "2",
|
|
50
|
+
"app_version": "1.2.0",
|
|
51
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Internal cryptographic helpers for Jackery API authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from Crypto.Cipher import AES, PKCS1_v1_5
|
|
9
|
+
from Crypto.PublicKey import RSA
|
|
10
|
+
from Crypto.Util.Padding import pad
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def aes_ecb_encrypt(plaintext: bytes, key: bytes) -> bytes:
|
|
14
|
+
"""AES/ECB/PKCS5Padding encryption (used for HTTP login body)."""
|
|
15
|
+
cipher = AES.new(key, AES.MODE_ECB)
|
|
16
|
+
result: bytes = cipher.encrypt(pad(plaintext, AES.block_size))
|
|
17
|
+
return result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def rsa_encrypt(plaintext: bytes, pub_key_b64: str) -> bytes:
|
|
21
|
+
"""RSA/ECB/PKCS1Padding encryption (used for encrypting AES key)."""
|
|
22
|
+
der = base64.b64decode(pub_key_b64)
|
|
23
|
+
key = RSA.import_key(der)
|
|
24
|
+
cipher = PKCS1_v1_5.new(key)
|
|
25
|
+
result: bytes = cipher.encrypt(plaintext)
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def aes_cbc_encrypt(plaintext: bytes, key: bytes, iv: bytes) -> bytes:
|
|
30
|
+
"""AES/CBC/PKCS5Padding encryption (used for MQTT password derivation)."""
|
|
31
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
32
|
+
result: bytes = cipher.encrypt(pad(plaintext, AES.block_size))
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def derive_mqtt_password(username: str, mqtt_password_b64: str) -> str:
|
|
37
|
+
"""Derive the MQTT connection password from the stored mqttPassWord.
|
|
38
|
+
|
|
39
|
+
The app does: AES/CBC/PKCS5Padding(username, key=b64decode(mqttPassWord), iv=key[:16])
|
|
40
|
+
then base64-encodes the result and sends it as the MQTT password string.
|
|
41
|
+
"""
|
|
42
|
+
key = base64.b64decode(mqtt_password_b64)
|
|
43
|
+
iv = key[:16]
|
|
44
|
+
encrypted = aes_cbc_encrypt(username.encode("utf-8"), key, iv)
|
|
45
|
+
return base64.b64encode(encrypted).decode("ascii")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_mac_id() -> str:
|
|
49
|
+
"""Generate a stable MAC-like identifier for this machine."""
|
|
50
|
+
node = uuid.getnode()
|
|
51
|
+
return ":".join(f"{(node >> (8 * i)) & 0xFF:02x}" for i in range(5, -1, -1))
|