4.4 KiB
Battery PoC — direct-connect charge state
Reads the aggregate battery percentage straight from the aGate over its own
SoftAP. No cloud, no MQTT, no Bluetooth. Recovered from the Dart AOT layer;
see RE_DART_FINDINGS.md §§3–5, 8, 10.
Requirements
- Host whose Wi-Fi adapter is associated to the device AP
(
AP_<sn[11:]>/<sn[8:]>,wifi.derive_hotspot_credentials). - L2 reachability to
10.100.1.1. In a container this means a bridgedmacvlan/ipvlanaddress on10.100.1.0/24ornetwork_mode: host(see repo-root.devcontainer/docker-compose.yml). A default Docker bridge (172.17.0.0/16) is NATed to a non-10.100.1.xsource and getsECONNREFUSED. - Python
requestsfor the HTTP helpers indevice_local.py.
What it does
- Probes TCP ports
9000, 18000in order (9000was open on the tested unit;0x4650 = 18000is the app constant but was closed there). - Sends
DEVICE_APOWER_INFO (0x19)withdata={"update": 2}(exactly what the app'squeryAPowerDatapasses asarguments), thenBMS (0x1D),EMS (0x1E): compact-JSONCommunicationObject(cmdType, equipNo, type, timeStamp, snno, len, crc[, dataArea], nodatakey) →encrypt_object(S1plaintext header first, tail(byte + abs_index + n) & 0xFFwithn = int(key, 32)) → sent raw over the socket (no length prefix —ClientSocket.sendforwards to_Socket.adddirectly). - Accumulates reply chunks and runs the app's
decryptDatalogic (second0x58marker →s + "}"equipNo probe → tail decode →s + "," + T), then extractscurrentBatterySoc→soc→avg(socArray).
Usage
# show derived hotspot creds only
python -m franklinwh_agate_client.battery --sn <20-char-SN> --show-hotspot
# print just the percentage
python -m franklinwh_agate_client.battery --sn <20-char-SN>
# full reply envelope
python -m franklinwh_agate_client.battery --sn <20-char-SN> --raw
# low-level probe: exact TX bytes + exact RX bytes per request variant
python -m franklinwh_agate_client.battery --sn <20-char-SN> --probe
Options: --host (default 10.100.1.1), --ports 9000,18000,
--key-radix {16,32} (default 32, per the radix: 32 literal at both
tryParse sites), --timeout.
Programmatic:
from franklinwh_agate_client import get_battery
out = get_battery("...", host="10.100.1.1")
print(out["value"], out["source"]) # e.g. 78.0 currentBatterySoc
Host probe recipe (run on the machine joined to the device AP)
The agent container cannot reach 10.100.1.x; run this on the host:
cd franklinwh-agate-client
python3 -m franklinwh_agate_client.battery \
--sn A03X2516013210060005 --host 10.100.1.1 --ports 9000,18000 --probe
Prefer the standalone scripts in
scripts/— they need no install and track the latest corrections:host_probe4.pyis current (socket-space ids +opt-seeded payloads). Copy the file content and runpython3 host_probe4.py --sn A03X2516013210060005 --ports 9000.
Expected: secret key for ...: 1c (radix 32), then per-variant tx
(encrypted envelope, starts with the ASCII header
{"cmdType":25,"equipNo":"A03X2516013210060005",) and rx bytes.
A rejected frame is met with zero bytes + close; any non-empty rx is
the device answering — paste the rx hex back and the reply parses via
decrypt_data.
If every variant still returns zero bytes, capture one app-driven session
instead: install the Frida hook (blutter_out/blutter_frida.js pattern —
log Socket Manager send object: / receive object: ), tap Refresh in
the app while joined to the AP, and compare its TX bytes against ours.
Status (2026-10-16) — live round-trip works
Two-way comms verified against 10.100.1.1:9000 from a host on
10.100.1.0/24: discovery → 1102 device-info reply (versions, FHP_SN,
BMS_SN), per-pack BMS reads ({"id": 1} / {"id": 2} → live batSoc),
EMS telemetry. get_battery() runs that exact sequence (discovery,
apower, BMS packs 1–2, EMS) and returns the first SoC found; an all-zero
BMS slot is skipped, not reported as 0 %.
DEVICE_APOWER_INFO answers {"result": 1, "reason": -1} on the tested
unit — SoC comes from the BMS packs instead.
Remaining limits:
- Needs L2 reachability to
10.100.1.1(host on the device AP or bridged container); the app's own socket port18000is closed on this unit. - BLE field order and
X-Device-*binding remain open (RE_DART_FINDINGS.mdOpen items).