franklinwh-direct-connect-api 0.3.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.
- franklinwh_direct_connect_api-0.3.0/LICENSE +21 -0
- franklinwh_direct_connect_api-0.3.0/PKG-INFO +394 -0
- franklinwh_direct_connect_api-0.3.0/README.md +339 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/PKG-INFO +394 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/SOURCES.txt +34 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/dependency_links.txt +1 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/entry_points.txt +2 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/requires.txt +7 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/top_level.txt +1 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/__init__.py +89 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/__main__.py +10 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/bms.py +230 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/catalog.py +609 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/cli.py +1634 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/client.py +671 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/discover.py +260 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/emulator.py +250 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/energy.py +251 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/probe.py +352 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/protocol.py +358 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/proxy.py +207 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/py.typed +0 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/synthetic.py +451 -0
- franklinwh_direct_connect_api-0.3.0/franklinwh_local/transport.py +247 -0
- franklinwh_direct_connect_api-0.3.0/pyproject.toml +66 -0
- franklinwh_direct_connect_api-0.3.0/setup.cfg +4 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_bms.py +172 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_cli_surface.py +186 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_client.py +390 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_emulator_dynamic.py +203 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_emulator_scan.py +165 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_energy.py +219 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_generator_writes.py +183 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_probe.py +337 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_protocol.py +177 -0
- franklinwh_direct_connect_api-0.3.0/tests/test_proxy_analyze.py +94 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 david2069
|
|
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,394 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: franklinwh-direct-connect-api
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Unofficial Python library and CLI for the FranklinWH aGate local broker protocol (TCP/9000 sendMqtt cmdType frames)
|
|
5
|
+
Author-email: david2069 <david.hona@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 david2069
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/david2069/franklinwh-local
|
|
29
|
+
Project-URL: Repository, https://github.com/david2069/franklinwh-local
|
|
30
|
+
Project-URL: Documentation, https://github.com/david2069/franklinwh-local/blob/main/docs/USAGE.md
|
|
31
|
+
Project-URL: Changelog, https://github.com/david2069/franklinwh-local/blob/main/CHANGELOG.md
|
|
32
|
+
Project-URL: Issues, https://github.com/david2069/franklinwh-local/issues
|
|
33
|
+
Project-URL: Companion (Cloud API), https://github.com/david2069/franklinwh-cloud
|
|
34
|
+
Keywords: franklinwh,agate,apower,battery,solar,energy-storage,home-automation,mqtt,local
|
|
35
|
+
Classifier: Development Status :: 3 - Alpha
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Topic :: Home Automation
|
|
44
|
+
Classifier: Topic :: System :: Networking
|
|
45
|
+
Classifier: Typing :: Typed
|
|
46
|
+
Requires-Python: >=3.10
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
License-File: LICENSE
|
|
49
|
+
Provides-Extra: test
|
|
50
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
51
|
+
Provides-Extra: docs
|
|
52
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
53
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# franklinwh-local
|
|
57
|
+
|
|
58
|
+
Unofficial Python library and CLI for the **FranklinWH aGate local broker protocol** —
|
|
59
|
+
the JSON `cmdType` frames exchanged over **TCP/9000**. This is the lower-level
|
|
60
|
+
device↔broker channel that the cloud `sendMqtt` REST relay (see
|
|
61
|
+
[`franklinwh-cloud`](https://github.com/david2069/franklinwh-cloud)) is layered on top of.
|
|
62
|
+
|
|
63
|
+
> **Status: alpha.** The protocol was reverse-engineered from packet captures.
|
|
64
|
+
> Frame parsing/encoding and the offline tooling are well tested; live transport
|
|
65
|
+
> against real hardware should be validated on your own LAN.
|
|
66
|
+
|
|
67
|
+
## Why this exists
|
|
68
|
+
|
|
69
|
+
The cloud REST API drops most of the structural telemetry (battery cell voltages,
|
|
70
|
+
relay states, full physics arrays) to speed up the mobile app. That data still
|
|
71
|
+
flows over the local broker channel. `franklinwh-local` speaks that channel
|
|
72
|
+
directly, so you can read deep telemetry and device configuration locally.
|
|
73
|
+
|
|
74
|
+
**Docs:** [USAGE.md](docs/USAGE.md) (full how-to) · [PROTOCOL.md](docs/PROTOCOL.md) (wire format + command catalog) · [API.md](docs/API.md) (API reference)
|
|
75
|
+
|
|
76
|
+
A rendered docs site (MkDocs Material) is published from `docs/` via GitHub
|
|
77
|
+
Actions. Build it locally with:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install -e ".[docs]"
|
|
81
|
+
mkdocs serve # http://127.0.0.1:8000
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Setup
|
|
85
|
+
|
|
86
|
+
Requires **Python 3.10+**. No runtime dependencies (standard library only).
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git clone https://github.com/david2069/franklinwh-local
|
|
90
|
+
cd franklinwh-local
|
|
91
|
+
|
|
92
|
+
python3 -m venv .venv # 1. create a virtualenv
|
|
93
|
+
source .venv/bin/activate # 2. activate it (Windows: .venv\Scripts\activate)
|
|
94
|
+
pip install -e . # 3. install this checkout (editable)
|
|
95
|
+
|
|
96
|
+
franklinwh-local --version # 4. verify
|
|
97
|
+
# franklinwh-local 0.1.0
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Add `".[test]"` instead of `"."` in step 3 if you want to run the pytest suite.
|
|
101
|
+
From PyPI (no checkout needed) the install is `pip install franklinwh-direct-connect-api`.
|
|
102
|
+
|
|
103
|
+
### How to run it
|
|
104
|
+
|
|
105
|
+
`franklinwh-local` is a **console script** that `pip install` puts on your
|
|
106
|
+
`PATH` — it is *not* a `.py` file in this repo, so these do **not** work:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python franklinwh-local ... # can't open file '.../franklinwh-local'
|
|
110
|
+
python franklinwh_local.py ... # no such file
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Run it one of these three ways:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
franklinwh-local catalog # venv activated (recommended)
|
|
117
|
+
.venv/bin/franklinwh-local catalog # venv not activated — call it by path
|
|
118
|
+
python -m franklinwh_local catalog # module form; works anywhere the package imports
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The names are easy to mix up: the PyPI distribution is
|
|
122
|
+
**`franklinwh-direct-connect-api`**, the import package is **`franklinwh_local`**
|
|
123
|
+
(underscore), and the CLI command is **`franklinwh-local`** (hyphen).
|
|
124
|
+
|
|
125
|
+
Everything below assumes the venv is activated. Offline commands
|
|
126
|
+
(`catalog`, `decode`, `analyze`, `emulate`) need no hardware — start with
|
|
127
|
+
`franklinwh-local catalog` to confirm your install works.
|
|
128
|
+
|
|
129
|
+
### Troubleshooting the install
|
|
130
|
+
|
|
131
|
+
| Symptom | Cause / fix |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `can't open file '.../franklinwh-local'` | You ran `python franklinwh-local`. Drop the `python` — it's a command, not a script. |
|
|
134
|
+
| `command not found: franklinwh-local` | The venv isn't activated (`source .venv/bin/activate`), or `pip install -e .` hasn't been run in it. |
|
|
135
|
+
| `No module named franklinwh_local` | You're on a different interpreter than the one you installed into. Check with `which python` and `pip -V` — both should point inside `.venv`. |
|
|
136
|
+
| `error: externally-managed-environment` | You ran `pip install` outside a venv on a Homebrew/system Python. Create and activate the venv first. |
|
|
137
|
+
|
|
138
|
+
## Quick start
|
|
139
|
+
|
|
140
|
+
Decode a capture (offline, no hardware):
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from franklinwh_local import iter_pcap_frames
|
|
144
|
+
|
|
145
|
+
for frame in iter_pcap_frames("capture.pcap"):
|
|
146
|
+
print(frame, frame.data_area)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Build and parse a single frame:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from franklinwh_local import encode_frame, decode_frame
|
|
153
|
+
|
|
154
|
+
wire = encode_frame(1301, "FAKEGATE90FJ09J6H4F2", {"opt": 0}) # power-flow poll
|
|
155
|
+
frame = decode_frame(wire)
|
|
156
|
+
assert frame.verify() # len + crc check
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Talk to a live gateway:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from franklinwh_local import LocalClient
|
|
163
|
+
|
|
164
|
+
with LocalClient("10.100.1.1") as c: # the hotspot gateway IP
|
|
165
|
+
c.login() # 1101 -> 1102 (firmware manifest)
|
|
166
|
+
print(c.power_flow()["soc"])
|
|
167
|
+
print(c.smart_circuits())
|
|
168
|
+
|
|
169
|
+
bms = c.battery_cells(1) # per-cell telemetry, the deep read
|
|
170
|
+
print(bms["batVolt"]) # [3326, 3326, ...] mV per cell
|
|
171
|
+
print(bms["batTemp"]) # [18.8, 18.9, ...] degC per cell
|
|
172
|
+
print(bms["batSoh"]) # 94.9
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Battery Management
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
franklinwh-local -i 192.0.2.110 battery # per-cell view
|
|
179
|
+
franklinwh-local -i 192.0.2.110 battery --watch 2 --for 10m
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Per-cell voltages and temperatures, pack SoC/SoH, bus rails and states — one session,
|
|
183
|
+
local only. `--watch`/`--for`/`--count` monitor continuously; Ctrl-C stops.
|
|
184
|
+
|
|
185
|
+
### Per-device reads
|
|
186
|
+
|
|
187
|
+
Four reads are addressed to a **specific device** rather than the gateway, and need an
|
|
188
|
+
`id` from `device_check` / `battery_modules` (`devMap[].id`):
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
franklinwh-local -i 192.0.2.110 battery_cells --id 1 # per-cell V + temps, SoC/SoH
|
|
192
|
+
franklinwh-local -i 192.0.2.110 power_electronics --id 1 # bus/grid/inverter V and A
|
|
193
|
+
franklinwh-local -i 192.0.2.110 device_firmware --id 1 # PE/BMS serials + versions
|
|
194
|
+
franklinwh-local -i 192.0.2.110 device_states --id 1 # DSP/main/PE/BMS states
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Called **without** `id` they return the same keys with empty or zero values
|
|
198
|
+
(`result=1 reason=-1`) — easy to misread as "this command returns nothing".
|
|
199
|
+
|
|
200
|
+
### Connecting (Direct Connection)
|
|
201
|
+
|
|
202
|
+
The aGate exposes TCP/9000 on its **own WiFi hotspot** — the same "Direct
|
|
203
|
+
Connect" path the FranklinWH mobile app uses. To talk to it:
|
|
204
|
+
|
|
205
|
+
1. On the gateway/app, start **Direct Connect → Connect via aGate hotspot**.
|
|
206
|
+
2. Join the WiFi network `AP_<serial-suffix>` (e.g. `AP_F24170091`).
|
|
207
|
+
3. The gateway is the hotspot gateway IP (typically `10.100.1.1`). Point
|
|
208
|
+
`LocalClient` / the CLI at it, or let the scanner find it:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
franklinwh-local scan gateway # auto-targets the hotspot gateway
|
|
212
|
+
franklinwh-local --host 10.100.1.1 power_flow
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
(The app also offers a Bluetooth Direct Connect path; only the WiFi/TCP path is
|
|
216
|
+
implemented here.)
|
|
217
|
+
|
|
218
|
+
### Discover devices on the LAN
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
franklinwh-local scan 10.100.1.0/24 # scan a subnet
|
|
222
|
+
franklinwh-local scan gateway # just the hotspot gateway
|
|
223
|
+
franklinwh-local scan 10.0.0.0/24 --json
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Detects open **TCP 9000** (sendMqtt) and **TCP 502** (Modbus), and by default
|
|
227
|
+
*confirms* a 9000 listener by performing the login handshake.
|
|
228
|
+
|
|
229
|
+
### Test without hardware (emulator)
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
franklinwh-local emulate --port 9000 # fake aGate replaying captured data
|
|
233
|
+
# then, in another shell:
|
|
234
|
+
franklinwh-local --host 127.0.0.1 power_flow
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## CLI
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
franklinwh-local catalog # list known cmdType codes (grouped)
|
|
241
|
+
franklinwh-local catalog --grep bms # filter by name/description/cloud call
|
|
242
|
+
franklinwh-local catalog --json # ... as JSON, with cloud equivalents
|
|
243
|
+
franklinwh-local decode capture.pcap # decode a pcap
|
|
244
|
+
franklinwh-local decode capture.pcap --json # ... as JSON
|
|
245
|
+
|
|
246
|
+
franklinwh-local --host 10.100.1.1 power_flow # live read
|
|
247
|
+
franklinwh-local --host 10.100.1.1 call 1301 --data '{"opt":0}'
|
|
248
|
+
|
|
249
|
+
franklinwh-local --host 10.100.1.1 power_flow --watch 5 # poll every 5s
|
|
250
|
+
franklinwh-local --host 10.100.1.1 power_flow --watch 5 --count 12
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Every name in `catalog` is a live subcommand** — the command list is generated from
|
|
254
|
+
the catalog, so the two cannot drift. `franklinwh-local -i <ip> grid_ov_trip` works just
|
|
255
|
+
as `power_flow` does, and anything uncatalogued is still reachable via `call <code>`.
|
|
256
|
+
|
|
257
|
+
`--watch [SECONDS]` (default 5) reuses one logged-in session and prints a line
|
|
258
|
+
per poll — a friendly summary for `power_flow`, compact JSON otherwise.
|
|
259
|
+
`--count N` stops after N samples (Ctrl-C otherwise).
|
|
260
|
+
|
|
261
|
+
## Control (live writes)
|
|
262
|
+
|
|
263
|
+
Confirmed working over LAN TCP/9000 (no SPAN unlock, no cloud) — see
|
|
264
|
+
[`catalog.WRITES`](franklinwh_local/catalog.py) and `docs/PROTOCOL.md`. The CLI
|
|
265
|
+
shape mirrors the sibling tools: `-i/--ip` for the aGate (as in
|
|
266
|
+
[franklinwh-modbus](https://github.com/david2069/franklinwh-modbus)), and a
|
|
267
|
+
get-or-`--set` command (as in
|
|
268
|
+
[franklinwh-cloud](https://github.com/david2069/franklinwh-cloud)):
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
franklinwh-local -i 192.0.2.110 mode # just the current mode
|
|
272
|
+
franklinwh-local -i 192.0.2.110 mode --list # all modes + details
|
|
273
|
+
franklinwh-local -i 192.0.2.110 mode --json # decoded dataArea (JSON)
|
|
274
|
+
franklinwh-local -i 192.0.2.110 mode --raw # full on-the-wire frame (cmdType/snno/crc/...)
|
|
275
|
+
franklinwh-local -i 192.0.2.110 mode --set self # set; alias tou / self / sc / backup
|
|
276
|
+
franklinwh-local -i 192.0.2.110 mode --set 29287 # ...or a programme id
|
|
277
|
+
|
|
278
|
+
franklinwh-local -i 192.0.2.110 offgrid # show off-grid status
|
|
279
|
+
franklinwh-local -i 192.0.2.110 offgrid --set on --soc 5
|
|
280
|
+
franklinwh-local -i 192.0.2.110 offgrid --set off # reconnect
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
In Python:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from franklinwh_local import LocalClient
|
|
287
|
+
with LocalClient("192.0.2.110") as c:
|
|
288
|
+
c.login()
|
|
289
|
+
c.set_mode("tou") # 1727 {opt:3, current_id}; alias or full name or id
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
> These command a real battery system. `mode --set` reports `current_id`
|
|
293
|
+
> before→after so you can confirm it took. Some settings (e.g. smart-circuit SoC
|
|
294
|
+
> cut-off) still round-trip through the cloud — use `… call <cmd> --data '…'` to
|
|
295
|
+
> experiment with codes not yet wrapped.
|
|
296
|
+
|
|
297
|
+
## Finding new (write/control) commands
|
|
298
|
+
|
|
299
|
+
The catalog so far is reverse-engineered from *read* traffic. To discover the
|
|
300
|
+
**write/control** codes, capture the app while it issues a control action and
|
|
301
|
+
look for `cmdType`s not yet in the catalog. You don't need Wireshark — macOS/Linux
|
|
302
|
+
ship `tcpdump`, and this library de-obfuscates the frames (Wireshark would only
|
|
303
|
+
show ciphered bytes):
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# 1. capture phone <-> aGate :9000 (Direct-Connect hotspot or LAN), then in the
|
|
307
|
+
# app do ONE control action at a time (switch mode, start charge, toggle SC).
|
|
308
|
+
sudo tcpdump -i <iface> -s0 -w fwh.pcap 'tcp port 9000'
|
|
309
|
+
|
|
310
|
+
# 2. triage — lists every cmdType seen; flags the ones NOT in the catalog:
|
|
311
|
+
franklinwh-local analyze fwh.pcap
|
|
312
|
+
franklinwh-local analyze fwh.pcap --unknown # just the candidate write codes
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Or watch the conversation live with a **transparent decoding proxy** — redirect
|
|
316
|
+
the app's `:9000` to it (ARP-spoof + DNAT, or a host route); it forwards every
|
|
317
|
+
byte verbatim and prints each decoded frame, flagging unknown cmdTypes:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
franklinwh-local proxy 192.0.2.110 # relay :9000 -> aGate:9000
|
|
321
|
+
franklinwh-local proxy 192.0.2.110 --listen 0.0.0.0:9000
|
|
322
|
+
franklinwh-local proxy 192.0.2.110 --record session.pcap # capture on demand
|
|
323
|
+
# session.pcap opens in Wireshark and re-decodes: franklinwh-local decode session.pcap
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> Unknown **odd** codes captured during a control action are the requests; their
|
|
327
|
+
> `dataArea` carries the setpoint. Add confirmed codes to `catalog.py`.
|
|
328
|
+
|
|
329
|
+
Or sweep the gateway directly with **`probe`**, which tries several *request payload
|
|
330
|
+
shapes* per code rather than only the usual `{"opt":0}`:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
franklinwh-local -i 192.0.2.110 probe # all uncatalogued odd codes
|
|
334
|
+
franklinwh-local -i 192.0.2.110 probe --range 1413-1699 # one block
|
|
335
|
+
franklinwh-local -i 192.0.2.110 probe --json > probe.json
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Some reads are addressed **per battery**, not per gateway — the cloud API's per-cell BMS
|
|
339
|
+
read sends `{"fhpSn": "<aPower serial>", "type": 2|3}`. A code like that answers a plain
|
|
340
|
+
`{"opt":0}` probe with silence, so a single-shape sweep cannot find it. `probe`
|
|
341
|
+
auto-discovers the battery serials (from `battery_modules` + the login manifest), tries
|
|
342
|
+
each shape, reports which one produced data, and flags replies that look like per-cell
|
|
343
|
+
telemetry. It only ever sends reads — never `opt=1`.
|
|
344
|
+
|
|
345
|
+
> **Caution:** this sends cmdTypes whose semantics are unknown to a real battery system.
|
|
346
|
+
> The payload matrix is read-shaped by construction, but scope your sweeps (`--codes`,
|
|
347
|
+
> `--range`) rather than blanket-probing a production gateway.
|
|
348
|
+
|
|
349
|
+
## How the protocol works
|
|
350
|
+
|
|
351
|
+
A frame is one JSON object. The header is cleartext; everything from `"type"`
|
|
352
|
+
onward is obfuscated with a trivial position-based additive cipher:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
{"cmdType":<N>,"equipNo":"<E>", <- cleartext header
|
|
356
|
+
"type":...,"timeStamp":...,"snno":...,"len":<L>,"crc":"<C>","dataArea":{...}}
|
|
357
|
+
\------------------------ ciphered region (i = 0 here) ----------------------/
|
|
358
|
+
|
|
359
|
+
encrypt: c[i] = (p[i] + seed + i) & 0xFF
|
|
360
|
+
decrypt: p[i] = (c[i] - seed - i) & 0xFF
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
There is **no key**. `seed` is `0x3F` for normal frames and `0xA5` for the
|
|
364
|
+
pre-login `1101` handshake. Because the first plaintext byte is always `"`,
|
|
365
|
+
decoding is self-synchronizing — the seed is recovered from the first ciphered
|
|
366
|
+
byte, so any frame can be read without knowing the seed in advance.
|
|
367
|
+
|
|
368
|
+
Field rules: `len` is the byte length of the compact `dataArea` JSON, and `crc`
|
|
369
|
+
is its CRC32 (uppercase hex). Full details and the command catalog are in
|
|
370
|
+
[`docs/PROTOCOL.md`](docs/PROTOCOL.md).
|
|
371
|
+
|
|
372
|
+
## Security note
|
|
373
|
+
|
|
374
|
+
The broker channel is plain TCP and the obfuscation is keyless, so anyone on the
|
|
375
|
+
LAN path can read and forge frames. Decoded payloads expose WiFi credentials,
|
|
376
|
+
device serials, firmware versions, and GPS/location. Treat the network segment
|
|
377
|
+
accordingly.
|
|
378
|
+
|
|
379
|
+
## Layout
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
franklinwh_local/
|
|
383
|
+
protocol.py cipher, Frame, encode/decode, FrameStream, pcap reader
|
|
384
|
+
catalog.py cmdType enum + descriptions
|
|
385
|
+
transport.py TCP client: connect, login, request/response
|
|
386
|
+
client.py high-level named commands
|
|
387
|
+
cli.py command-line entry point
|
|
388
|
+
tests/ pytest suite (driven by a real capture fixture)
|
|
389
|
+
docs/PROTOCOL.md protocol + command catalog
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## License
|
|
393
|
+
|
|
394
|
+
MIT. Unofficial and not affiliated with or endorsed by FranklinWH.
|