Files
franklinwh-agate-client/docs/BATTERY_POC.md
T

4.4 KiB
Raw Blame History

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 bridged macvlan/ipvlan address on 10.100.1.0/24 or network_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.x source and gets ECONNREFUSED.
  • Python requests for the HTTP helpers in device_local.py.

What it does

  1. Probes TCP ports 9000, 18000 in order (9000 was open on the tested unit; 0x4650 = 18000 is the app constant but was closed there).
  2. Sends DEVICE_APOWER_INFO (0x19) with data={"update": 2} (exactly what the app's queryAPowerData passes as arguments), then BMS (0x1D), EMS (0x1E): compact-JSON CommunicationObject (cmdType, equipNo, type, timeStamp, snno, len, crc[, dataArea], no data key) → encrypt_object (S1 plaintext header first, tail (byte + abs_index + n) & 0xFF with n = int(key, 32)) → sent raw over the socket (no length prefix — ClientSocket.send forwards to _Socket.add directly).
  3. Accumulates reply chunks and runs the app's decryptData logic (second 0x58 marker → s + "}" equipNo probe → tail decode → s + "," + T), then extracts currentBatterySoc → 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.py is current (socket-space ids + opt-seeded payloads). Copy the file content and run python3 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 port 18000 is closed on this unit.
  • BLE field order and X-Device-* binding remain open (RE_DART_FINDINGS.md Open items).