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.
Files changed (36) hide show
  1. franklinwh_direct_connect_api-0.3.0/LICENSE +21 -0
  2. franklinwh_direct_connect_api-0.3.0/PKG-INFO +394 -0
  3. franklinwh_direct_connect_api-0.3.0/README.md +339 -0
  4. franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/PKG-INFO +394 -0
  5. franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/SOURCES.txt +34 -0
  6. franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/dependency_links.txt +1 -0
  7. franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/entry_points.txt +2 -0
  8. franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/requires.txt +7 -0
  9. franklinwh_direct_connect_api-0.3.0/franklinwh_direct_connect_api.egg-info/top_level.txt +1 -0
  10. franklinwh_direct_connect_api-0.3.0/franklinwh_local/__init__.py +89 -0
  11. franklinwh_direct_connect_api-0.3.0/franklinwh_local/__main__.py +10 -0
  12. franklinwh_direct_connect_api-0.3.0/franklinwh_local/bms.py +230 -0
  13. franklinwh_direct_connect_api-0.3.0/franklinwh_local/catalog.py +609 -0
  14. franklinwh_direct_connect_api-0.3.0/franklinwh_local/cli.py +1634 -0
  15. franklinwh_direct_connect_api-0.3.0/franklinwh_local/client.py +671 -0
  16. franklinwh_direct_connect_api-0.3.0/franklinwh_local/discover.py +260 -0
  17. franklinwh_direct_connect_api-0.3.0/franklinwh_local/emulator.py +250 -0
  18. franklinwh_direct_connect_api-0.3.0/franklinwh_local/energy.py +251 -0
  19. franklinwh_direct_connect_api-0.3.0/franklinwh_local/probe.py +352 -0
  20. franklinwh_direct_connect_api-0.3.0/franklinwh_local/protocol.py +358 -0
  21. franklinwh_direct_connect_api-0.3.0/franklinwh_local/proxy.py +207 -0
  22. franklinwh_direct_connect_api-0.3.0/franklinwh_local/py.typed +0 -0
  23. franklinwh_direct_connect_api-0.3.0/franklinwh_local/synthetic.py +451 -0
  24. franklinwh_direct_connect_api-0.3.0/franklinwh_local/transport.py +247 -0
  25. franklinwh_direct_connect_api-0.3.0/pyproject.toml +66 -0
  26. franklinwh_direct_connect_api-0.3.0/setup.cfg +4 -0
  27. franklinwh_direct_connect_api-0.3.0/tests/test_bms.py +172 -0
  28. franklinwh_direct_connect_api-0.3.0/tests/test_cli_surface.py +186 -0
  29. franklinwh_direct_connect_api-0.3.0/tests/test_client.py +390 -0
  30. franklinwh_direct_connect_api-0.3.0/tests/test_emulator_dynamic.py +203 -0
  31. franklinwh_direct_connect_api-0.3.0/tests/test_emulator_scan.py +165 -0
  32. franklinwh_direct_connect_api-0.3.0/tests/test_energy.py +219 -0
  33. franklinwh_direct_connect_api-0.3.0/tests/test_generator_writes.py +183 -0
  34. franklinwh_direct_connect_api-0.3.0/tests/test_probe.py +337 -0
  35. franklinwh_direct_connect_api-0.3.0/tests/test_protocol.py +177 -0
  36. 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.