Make Wi-Fi join/leave actually functional

wifi.py presented itself as a join/leave abstraction but provided no real
behavior: the default backend was a no-op, the facade never verified the
association, and the nmcli queries were wrong. This makes the module do what
it claims, verified end-to-end against the recovered protocol and nmcli 1.52.

wifi.py
  - WifiManager: join now verifies the association by polling
    backend.is_connected against a settle_timeout (fail-closed). An
    unverified join raises HotspotJoinError and leaves connected=False;
    verify=False restores fire-and-forget. Tracks last_profile and adds
    gateway_ip().
  - NmcliWifiBackend: the association check now uses the ACTIVE column
    (`device wifi list -f SSID,ACTIVE`). The old `device wifi show` was a
    prefix-match accident onto `show-password` and reported in-range APs, not
    the associated one. get_ip() queries per-Wi-Fi-interface IP4.ADDRESS
    (preferring the 10.100 aGate subnet) instead of the unfiltered all-device
    dump that would return a wired/VPN address. Signal is read from the active
    AP; the dead ipcmd param is removed; leave() is best-effort.
  - WifiBackend: documented the fail-closed contract and added get_signal()
    so the manager no longer reaches into a private _run via isinstance.
  - NoOpWifiBackend: added assume_connected() for manual/external joins.

client.py
  - AgateClient now owns a WifiManager and exposes join_hotspot(),
    leave_hotspot(), and attach_local() (join + verify + LocalDeviceClient
    aimed at the device gateway). LocalDeviceClient is imported lazily so the
    package still works without `requests`.

__init__.py
  - Export the backends, HotspotProfile, HotspotJoinError, the SN-derivation
    helper, and the module constants.

tests/test_wifi.py (new)
  - 20 stdlib-unittest cases: SN derivation, verify pass/fail, fail-closed
    behavior, the in-range-but-not-associated regression, IP preference,
    tolerant leave, and the client wiring. No radio or pytest required.

Also:
  - README: documented the local-connect (device hotspot) flow.
  - .gitignore: ignore Python build/cache artifacts.
This commit is contained in:
kfj001 committed 2026-10-13 02:27:43 +00:00
1 parent a87dbe200e
commit 4c0b9de3df
6 files changed
+616 -78

No files matched your search

+38
View File
@@ -51,10 +51,48 @@ client = AgateClient()
device_info = client.query_agate_info(device_id)
```
## 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_<sn[11:]>` / `<sn[8:]>`, or the
`AP_<sn>` / `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.