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:
1 parent
a87dbe200e
commit
4c0b9de3df
6 files changed
+616
-78
No files matched your search
@@ -2,25 +2,69 @@
|
||||
Agate Client - Main interface for AGATE device operations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from .bluetooth import BluetoothManager
|
||||
from .wifi import HotspotProfile, WifiBackend, WifiManager
|
||||
|
||||
|
||||
class AgateClient:
|
||||
"""Main client for FranklinWH AGATE device operations."""
|
||||
|
||||
def __init__(self, device_id=None):
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None,
|
||||
wifi_backend: Optional[WifiBackend] = None) -> None:
|
||||
self.device_id = device_id
|
||||
self.bluetooth_manager = None
|
||||
self.wifi_manager = None
|
||||
|
||||
def query_agate_info(self, device_id=None):
|
||||
self.bluetooth_manager: Optional[BluetoothManager] = None
|
||||
self.wifi_manager = WifiManager(backend=wifi_backend)
|
||||
|
||||
# -- device hotspot (local connect) -------------------------------------
|
||||
def join_hotspot(self, sn: str, **kwargs: Any) -> HotspotProfile:
|
||||
"""Join the device SoftAP (derives creds, joins, verifies)."""
|
||||
return self.wifi_manager.join_device_hotspot(sn, **kwargs)
|
||||
|
||||
def leave_hotspot(self) -> None:
|
||||
"""Disconnect from the device hotspot."""
|
||||
self.wifi_manager.leave_device_hotspot()
|
||||
|
||||
def attach_local(self, sn: str, *, verify_join: bool = True,
|
||||
settle_timeout: float = 30.0, poll_interval: float = 1.0,
|
||||
**local_kwargs: Any):
|
||||
"""Join the device hotspot, then return a ``LocalDeviceClient``
|
||||
aimed at the device gateway (``https://10.100.1.1:18000`` by default).
|
||||
|
||||
``verify_join`` gates the association check (TLS verification of the
|
||||
device API is a separate concern — pass ``verify=`` in
|
||||
``local_kwargs``). Raises
|
||||
:class:`~franklinwh_agate_client.wifi.HotspotJoinError` if the
|
||||
association never verifies. (``LocalDeviceClient`` imports
|
||||
``requests``; install it before calling this on a cloud-less host.)
|
||||
|
||||
Extra keyword arguments are passed through to ``LocalDeviceClient``
|
||||
(``port``, ``scheme``, ``verify``, ``socket_port``, ``key_radix``,
|
||||
``timeout``, ``extra_headers``, ``session``).
|
||||
"""
|
||||
profile = self.wifi_manager.join_device_hotspot(
|
||||
sn, verify=verify_join, settle_timeout=settle_timeout,
|
||||
poll_interval=poll_interval,
|
||||
)
|
||||
from .device_local import LocalDeviceClient
|
||||
|
||||
return LocalDeviceClient(sn=sn, host=profile.gateway_ip, **local_kwargs)
|
||||
|
||||
# -- cloud / stub operations (pending RE) -------------------------------
|
||||
def query_agate_info(self, device_id: Optional[str] = None):
|
||||
"""Query AGATE device information."""
|
||||
# Reverse engineered operation: queryAgateInfo
|
||||
pass
|
||||
|
||||
def query_device_info_http(self, device_id=None):
|
||||
|
||||
def query_device_info_http(self, device_id: Optional[str] = None):
|
||||
"""Query device info via HTTP."""
|
||||
# Reverse engineered operation: queryDeviceInfoHttp
|
||||
pass
|
||||
|
||||
def query_modes_by_http(self, device_id=None):
|
||||
|
||||
def query_modes_by_http(self, device_id: Optional[str] = None):
|
||||
"""Query operating modes via HTTP."""
|
||||
# Reverse engineered operation: queryModesByHttp
|
||||
pass
|
||||
Reference in new issue
Block a user