# FranklinWH AGATE Client Python client library for interacting with FranklinWH AGATE energy storage device. ## Repository **Gitea:** http://192.168.1.74:30008/WesternTechnologies/franklinwh-agate-client ## Documentation - **[Dart AOT Reverse-Engineering Findings](docs/RE_DART_FINDINGS.md)** — detailed protocol analysis recovered from the app's `libapp.so` with [blutter](https://github.com/worawit/blutter) (transports, wire envelope, command/topic enums, crypto/CRC, BLE framing, hotspot credentials). - **[Battery PoC](docs/BATTERY_POC.md)** — read charge % over the SoftAP socket (`python -m franklinwh_agate_client.battery --sn `). ## Reverse Engineering Summary This library was reverse engineered from the official FranklinWH Family Energy Android app (`com.Franklinwh.FamilyEnergy`). ### Network I/O Stack The device communicates via: - **Bluetooth LE**: Direct connection to AGATE device - **Onboard Wi-Fi**: Local network connection via device hotspot - **Cloud APIs**: `https://e-api.franklinwh.com`, `https://energy.franklinwh.com` ### Key Operations Discovered - `queryAgateInfo` - Get device information - `queryDeviceInfoHttp` - HTTP device info query - `queryModesByHttp` - Query operating modes - `queryConnectWifi` - Wi-Fi connection management - `connectViaBluetooth` - Bluetooth connection - `startBluetoothHeartbeat` - Keepalive - `BluetoothManager` - Bluetooth service management - `WifiOnboarding` - Wi-Fi setup flows ### Endpoints Found - `https://e-api.franklinwh.com/api/appapi` - `https://energy.franklinwh.com/api-energy/deviceGroup/electric` - `https://energy.franklinwh.com/hes-gateway` - `https://dev.franklinwh.com/hes-gateway` - `https://uat.franklinwh.com/api-vpp-portal` ## Usage ```python from franklinwh_agate_client import AgateClient client = AgateClient() device_info = client.query_agate_info(device_id) ``` ## Battery PoC (SoftAP socket) ```bash python -m franklinwh_agate_client.battery --sn <20-char-SN> --show-hotspot python -m franklinwh_agate_client.battery --sn <20-char-SN> # e.g. 78% python -m franklinwh_agate_client.battery --sn <20-char-SN> --raw ``` Probes `10.100.1.1:9000,18000`, sends `0x19/0x1D/0x1E`, prints `currentBatterySoc → soc → avg(socArray)`. Must run where the Wi-Fi adapter is on the device AP — see [Battery PoC](docs/BATTERY_POC.md). ## Local connect (device hotspot) The aGate runs its own SoftAP (gateway `10.100.1.1`); joining it is what lets the client talk to the device directly. The client derives the hotspot credentials from the 20-char SN (`AP_` / ``, or the `AP_` / `12345678` fallback for malformed SNs), joins, **verifies the association**, then talks to the device over `https://10.100.1.1:18000` and the encrypted socket channel. ```python from franklinwh_agate_client import ( AgateClient, NmcliWifiBackend, NoOpWifiBackend, derive_hotspot_credentials, ) # Linux desktop/laptop with NetworkManager: client = AgateClient(wifi_backend=NmcliWifiBackend()) profile = client.join_hotspot(sn) # joins + verifies; raises HotspotJoinError on failure local = client.attach_local(sn) # LocalDeviceClient -> https://10.100.1.1:18000 info = local.obtain_agate_info() # or local.query_bms_info(), ... # Container, or a host where you join the hotspot manually: wm = AgateClient(wifi_backend=NoOpWifiBackend(local_ip="10.100.1.2")).wifi_manager wm.assume_connected(derive_hotspot_credentials(sn)) ``` `WifiManager.join_*` are fail-closed: they poll the backend's `is_connected` against `settle_timeout` (default 30 s) and raise `HotspotJoinError` if the association never confirms (`verify=False` for fire-and-forget). On Linux, `NmcliWifiBackend` uses `nmcli ... device wifi list -f SSID,ACTIVE` for the association check (a merely *visible* AP does not count) and per-Wi-Fi- interface `IP4.ADDRESS` lookups for the host IP. ## Development This library is a work in progress. Additional operations will be added as reverse engineering continues. Tests (no radio or NetworkManager required): ``` python -m unittest discover -s tests ``` ## License Internal use only. Reverse engineering findings for research purposes.