Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60b32830f | ||
|
|
eda78dd7d1 | ||
|
|
3c7e74fd0e |
No files matched your search
@@ -396,6 +396,59 @@ Library: `power.get_power()` tries detail → load → EMS in order
|
|||||||
(`franklinwh_agate_client/power.py`); `LocalDeviceClient` gained
|
(`franklinwh_agate_client/power.py`); `LocalDeviceClient` gained
|
||||||
`query_detail_info()` / `query_load_status()`.
|
`query_detail_info()` / `query_load_status()`.
|
||||||
|
|
||||||
|
## 17. Cloud account binding (installer flow)
|
||||||
|
|
||||||
|
Binding a unit to an account is an *installer commissioning* action, not
|
||||||
|
a local-socket one (`sections/installer/bind_device/`):
|
||||||
|
|
||||||
|
- `BindDeviceProvider.bind()` POSTs a data map to
|
||||||
|
**`RemoteInterface.deviceBind` → `POST {cloud}/manage/deviceBindV2`**,
|
||||||
|
with `protocolVer: "V1.10.01"`, the installer `jobCode`, the
|
||||||
|
`commissionId` (preset-site path via `InstallContext`), and the
|
||||||
|
`BindDeviceEntity` payload. Result parses as
|
||||||
|
`HttpResultEntity<BindDeviceEntity>` (bound `account`, distributor /
|
||||||
|
financier ids/names).
|
||||||
|
- Mirror calls: `queryDeviceBindData` →
|
||||||
|
`GET {cloud}/manage/getGatewayBindData` (pre-check: is this gateway
|
||||||
|
already bound, to whom) and `deviceUnbind` →
|
||||||
|
`POST {cloud}/manage/unBindDevice` ("Are you sure you want to
|
||||||
|
unbind?", `unbindSuccess` strings).
|
||||||
|
- Request auth on cloud calls is a signature, not a session cookie:
|
||||||
|
`RemoteService` computes **`MD5(jsonEncode(body) + "B6F3Zu&aW8XlH9CA")`**
|
||||||
|
and sends it as the **`token`** header (dio `Options`). User login
|
||||||
|
itself is Okta (`sunrun.okta.com`, MFA endpoints); the known cloud
|
||||||
|
bases are `https://e-api.franklinwh.com/api/appapi` and
|
||||||
|
`https://energy.franklinwh.com/*` (exact `/manage` base mapping lives
|
||||||
|
in the `RemoteService` dio init).
|
||||||
|
- Locally, bound devices land in the trusted-devices table
|
||||||
|
(`DatabaseManager.getTrustedDevicesTableList`, consulted by
|
||||||
|
`_checkConnectedNotTrustedAp` on direct-connect).
|
||||||
|
|
||||||
|
## 18. Joining the aGate to home Wi-Fi (STA onboarding)
|
||||||
|
|
||||||
|
Three transports can deliver the credentials; all verified in
|
||||||
|
disassembly, socket path live-tested for framing (not the join itself):
|
||||||
|
|
||||||
|
- **Socket** (while on the SoftAP): `queryConnectWifi` →
|
||||||
|
`CommunicationService.connectWifi(entity, …)` → `_request` with cmd
|
||||||
|
`connectWifi` (socket `1111`), operate **SET** (`type: 1`), arguments =
|
||||||
|
`ConnectWifiEntity.toJson()`: `ssid`, `password`, `wifi_SSID`,
|
||||||
|
`wifi_Pw` (plus an unencrypted-network flag). Library:
|
||||||
|
`LocalDeviceClient.join_home_wifi(ssid, password)` (+ `opt: 1`
|
||||||
|
seeding via the shared factory path), `scan_home_wifi()`
|
||||||
|
(`scanWifiList`, socket `1109`, no args) to list visible networks,
|
||||||
|
`query_connection_status()` (`connectionStatus`, socket `1113`) to
|
||||||
|
confirm association.
|
||||||
|
- **BLE**: ESP protocomm (`prov-session`, `prov-scan`, `prov-config`,
|
||||||
|
`custom-data`, sec0/sec1) — the standard Espressif flow; `prov-config`
|
||||||
|
carries the SSID/password. Used when the phone is not on the SoftAP.
|
||||||
|
- **Phone-side**: `wifi_iot` plugin join/leave is the *phone* joining the
|
||||||
|
*device* AP (`wifi.join_device_hotspot`) — the opposite direction.
|
||||||
|
|
||||||
|
.. warning:: `join_home_wifi` is state-changing: on success the unit
|
||||||
|
leaves its SoftAP (or roams), dropping the current session. Only run
|
||||||
|
it with physical/rejoin access to the unit.
|
||||||
|
|
||||||
## 9. Bluetooth (BLE) transport — `communication_center/bluetooth`
|
## 9. Bluetooth (BLE) transport — `communication_center/bluetooth`
|
||||||
|
|
||||||
- `BluetoothManager` (singleton), `BluetoothStatus`: `closed / connected / connecting / interrupted`.
|
- `BluetoothManager` (singleton), `BluetoothStatus`: `closed / connected / connecting / interrupted`.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from .device_local import LocalDeviceClient
|
from .device_local import LocalDeviceClient
|
||||||
from .protocol import CommunicationCmd
|
from .protocol import CommunicationCmd
|
||||||
from .wifi import derive_hotspot_credentials
|
from .wifi import DEVICE_GATEWAY_IP, derive_hotspot_credentials
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ def _is_empty_pack(reply: Dict[str, Any], result: Dict[str, Any]) -> bool:
|
|||||||
|
|
||||||
def get_battery(
|
def get_battery(
|
||||||
sn: str,
|
sn: str,
|
||||||
host: str = "10.100.1.1",
|
host: str = DEVICE_GATEWAY_IP,
|
||||||
ports: Tuple[int, ...] = SOCKET_PORT_CANDIDATES,
|
ports: Tuple[int, ...] = SOCKET_PORT_CANDIDATES,
|
||||||
key_radix: int = 16,
|
key_radix: int = 16,
|
||||||
timeout: float = 5.0,
|
timeout: float = 5.0,
|
||||||
@@ -197,7 +197,7 @@ PROBE_VARIANTS: Tuple[Tuple[str, CommunicationCmd, Any], ...] = (
|
|||||||
|
|
||||||
def probe_device(
|
def probe_device(
|
||||||
sn: str,
|
sn: str,
|
||||||
host: str = "10.100.1.1",
|
host: str = DEVICE_GATEWAY_IP,
|
||||||
ports: Tuple[int, ...] = SOCKET_PORT_CANDIDATES,
|
ports: Tuple[int, ...] = SOCKET_PORT_CANDIDATES,
|
||||||
key_radix: int = 16,
|
key_radix: int = 16,
|
||||||
timeout: float = 5.0,
|
timeout: float = 5.0,
|
||||||
@@ -262,7 +262,7 @@ _MQTT_CONNECT = bytes.fromhex("101200044d5154540402003c000670726f62653031")
|
|||||||
|
|
||||||
def probe_transport(
|
def probe_transport(
|
||||||
sn: str,
|
sn: str,
|
||||||
host: str = "10.100.1.1",
|
host: str = DEVICE_GATEWAY_IP,
|
||||||
ports: Tuple[int, ...] = (9000,),
|
ports: Tuple[int, ...] = (9000,),
|
||||||
key_radix: int = 16,
|
key_radix: int = 16,
|
||||||
timeout: float = 5.0,
|
timeout: float = 5.0,
|
||||||
@@ -327,7 +327,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
description="Read aGate battery percentage over the device SoftAP socket."
|
description="Read aGate battery percentage over the device SoftAP socket."
|
||||||
)
|
)
|
||||||
parser.add_argument("--sn", required=True, help="20-char device serial")
|
parser.add_argument("--sn", required=True, help="20-char device serial")
|
||||||
parser.add_argument("--host", default="10.100.1.1", help="aGate gateway IP")
|
parser.add_argument("--host", default=DEVICE_GATEWAY_IP, help="aGate gateway IP")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--ports", default=",".join(map(str, SOCKET_PORT_CANDIDATES)),
|
"--ports", default=",".join(map(str, SOCKET_PORT_CANDIDATES)),
|
||||||
help="comma-separated socket ports to probe",
|
help="comma-separated socket ports to probe",
|
||||||
|
|||||||
@@ -7,22 +7,39 @@ from __future__ import annotations
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from .bluetooth import BluetoothManager
|
from .bluetooth import BluetoothManager
|
||||||
from .wifi import HotspotProfile, WifiBackend, WifiManager
|
from .wifi import DEVICE_GATEWAY_IP, HotspotProfile, WifiBackend, WifiManager
|
||||||
|
|
||||||
|
|
||||||
class AgateClient:
|
class AgateClient:
|
||||||
"""Main client for FranklinWH AGATE device operations."""
|
"""Main client for FranklinWH AGATE device operations.
|
||||||
|
|
||||||
|
:param device_id: optional default device identifier.
|
||||||
|
:param wifi_backend: platform backend used for hotspot joins.
|
||||||
|
:param address: optional default aGate address (IP or hostname).
|
||||||
|
Overrides the ``10.100.1.1`` SoftAP default for direct-connect
|
||||||
|
calls — e.g. the home-LAN address (``192.168.1.x``) once the unit
|
||||||
|
is on the router. Per-call ``address``/``host``/``gateway_ip``
|
||||||
|
arguments take precedence over this default.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None,
|
def __init__(self, device_id: Optional[str] = None,
|
||||||
wifi_backend: Optional[WifiBackend] = None) -> None:
|
wifi_backend: Optional[WifiBackend] = None,
|
||||||
|
address: Optional[str] = None) -> None:
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
self.bluetooth_manager: Optional[BluetoothManager] = None
|
self.bluetooth_manager: Optional[BluetoothManager] = None
|
||||||
self.wifi_manager = WifiManager(backend=wifi_backend)
|
self.wifi_manager = WifiManager(backend=wifi_backend)
|
||||||
|
self.address = address
|
||||||
|
|
||||||
# -- device hotspot (local connect) -------------------------------------
|
# -- device hotspot (local connect) -------------------------------------
|
||||||
def join_hotspot(self, sn: str, **kwargs: Any) -> HotspotProfile:
|
def join_hotspot(self, sn: str, gateway_ip: Optional[str] = None,
|
||||||
"""Join the device SoftAP (derives creds, joins, verifies)."""
|
**kwargs: Any) -> HotspotProfile:
|
||||||
return self.wifi_manager.join_device_hotspot(sn, **kwargs)
|
"""Join the device SoftAP (derives creds, joins, verifies).
|
||||||
|
|
||||||
|
``gateway_ip`` overrides the address recorded on the profile
|
||||||
|
without changing the radio join.
|
||||||
|
"""
|
||||||
|
return self.wifi_manager.join_device_hotspot(
|
||||||
|
sn, gateway_ip=gateway_ip or self.address, **kwargs)
|
||||||
|
|
||||||
def leave_hotspot(self) -> None:
|
def leave_hotspot(self) -> None:
|
||||||
"""Disconnect from the device hotspot."""
|
"""Disconnect from the device hotspot."""
|
||||||
@@ -30,9 +47,23 @@ class AgateClient:
|
|||||||
|
|
||||||
def attach_local(self, sn: str, *, verify_join: bool = True,
|
def attach_local(self, sn: str, *, verify_join: bool = True,
|
||||||
settle_timeout: float = 30.0, poll_interval: float = 1.0,
|
settle_timeout: float = 30.0, poll_interval: float = 1.0,
|
||||||
|
address: Optional[str] = None,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
gateway_ip: Optional[str] = None,
|
||||||
|
join: Optional[bool] = None,
|
||||||
**local_kwargs: Any):
|
**local_kwargs: Any):
|
||||||
"""Join the device hotspot, then return a ``LocalDeviceClient``
|
"""Join the device hotspot, then return a ``LocalDeviceClient``.
|
||||||
aimed at the device gateway (``https://10.100.1.1:18000`` by default).
|
|
||||||
|
Without an explicit address this keeps the existing behaviour:
|
||||||
|
join the SoftAP and aim at the profile gateway
|
||||||
|
(``https://10.100.1.1:18000`` by default).
|
||||||
|
|
||||||
|
Pass ``address`` (``host``/``gateway_ip`` are aliases) to reach the
|
||||||
|
aGate elsewhere — e.g. its home-LAN IP once it is on the router.
|
||||||
|
An explicit address defaults to *skipping* the hotspot radio join
|
||||||
|
(pass ``join=True`` to force it); ``join=False`` never touches the
|
||||||
|
radio. The client-level ``address`` default applies when no
|
||||||
|
per-call address is given.
|
||||||
|
|
||||||
``verify_join`` gates the association check (TLS verification of the
|
``verify_join`` gates the association check (TLS verification of the
|
||||||
device API is a separate concern — pass ``verify=`` in
|
device API is a separate concern — pass ``verify=`` in
|
||||||
@@ -45,13 +76,21 @@ class AgateClient:
|
|||||||
(``port``, ``scheme``, ``verify``, ``socket_port``, ``key_radix``,
|
(``port``, ``scheme``, ``verify``, ``socket_port``, ``key_radix``,
|
||||||
``timeout``, ``extra_headers``, ``session``).
|
``timeout``, ``extra_headers``, ``session``).
|
||||||
"""
|
"""
|
||||||
|
explicit = address if address is not None else (
|
||||||
|
host if host is not None else gateway_ip)
|
||||||
|
target = explicit if explicit is not None else self.address
|
||||||
|
do_join = (target is None) if join is None else join
|
||||||
|
if do_join:
|
||||||
profile = self.wifi_manager.join_device_hotspot(
|
profile = self.wifi_manager.join_device_hotspot(
|
||||||
sn, verify=verify_join, settle_timeout=settle_timeout,
|
sn, verify=verify_join, settle_timeout=settle_timeout,
|
||||||
poll_interval=poll_interval,
|
poll_interval=poll_interval,
|
||||||
|
gateway_ip=target,
|
||||||
)
|
)
|
||||||
|
target = profile.gateway_ip
|
||||||
from .device_local import LocalDeviceClient
|
from .device_local import LocalDeviceClient
|
||||||
|
|
||||||
return LocalDeviceClient(sn=sn, host=profile.gateway_ip, **local_kwargs)
|
return LocalDeviceClient(sn=sn, host=target or DEVICE_GATEWAY_IP,
|
||||||
|
**local_kwargs)
|
||||||
|
|
||||||
# -- cloud / stub operations (pending RE) -------------------------------
|
# -- cloud / stub operations (pending RE) -------------------------------
|
||||||
def query_agate_info(self, device_id: Optional[str] = None):
|
def query_agate_info(self, device_id: Optional[str] = None):
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class LocalDeviceClient:
|
|||||||
if envelope is None:
|
if envelope is None:
|
||||||
envelope = direct_connect_request(
|
envelope = direct_connect_request(
|
||||||
cmd, self.sn if equip_no is None else equip_no, data=data,
|
cmd, self.sn if equip_no is None else equip_no, data=data,
|
||||||
snno=self._sn_seq.next(),
|
snno=self._sn_seq.next(), operate=operate,
|
||||||
)
|
)
|
||||||
return self.send_envelope(envelope)
|
return self.send_envelope(envelope)
|
||||||
|
|
||||||
@@ -212,6 +212,7 @@ class LocalDeviceClient:
|
|||||||
data: Optional[Dict[str, Any]] = None,
|
data: Optional[Dict[str, Any]] = None,
|
||||||
timeout: Optional[float] = None,
|
timeout: Optional[float] = None,
|
||||||
equip_no: Optional[str] = None,
|
equip_no: Optional[str] = None,
|
||||||
|
operate: CommunicationOperate = CommunicationOperate.QUERY,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Send one envelope, return exact TX/RX bytes plus any parsed reply.
|
"""Send one envelope, return exact TX/RX bytes plus any parsed reply.
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ class LocalDeviceClient:
|
|||||||
"""
|
"""
|
||||||
envelope = direct_connect_request(
|
envelope = direct_connect_request(
|
||||||
cmd, self.sn if equip_no is None else equip_no, data=data,
|
cmd, self.sn if equip_no is None else equip_no, data=data,
|
||||||
snno=self._sn_seq.next(),
|
snno=self._sn_seq.next(), operate=operate,
|
||||||
)
|
)
|
||||||
wire = self.codec.encode(envelope)
|
wire = self.codec.encode(envelope)
|
||||||
reply_raw = self._socket_roundtrip(
|
reply_raw = self._socket_roundtrip(
|
||||||
@@ -354,6 +355,34 @@ class LocalDeviceClient:
|
|||||||
def query_modes_v2(self) -> Dict[str, Any]:
|
def query_modes_v2(self) -> Dict[str, Any]:
|
||||||
return self.send_rpc(CommunicationCmd.MODE_V2)
|
return self.send_rpc(CommunicationCmd.MODE_V2)
|
||||||
|
|
||||||
|
def join_home_wifi(self, ssid: str, password: str = "") -> Dict[str, Any]:
|
||||||
|
"""``connectWifi`` — join the aGate to a home Wi-Fi network.
|
||||||
|
|
||||||
|
Mirrors ``WifiOnboardingConnectProvider``: sends a
|
||||||
|
``ConnectWifiEntity`` shaped payload (``ssid``/``password`` plus
|
||||||
|
the ``wifi_SSID``/``wifi_Pw`` mirrors) with operate SET over the
|
||||||
|
current transport. Follow with :meth:`query_connection_status`
|
||||||
|
to confirm association.
|
||||||
|
|
||||||
|
.. warning:: State-changing: on success the unit leaves its
|
||||||
|
SoftAP (or roams), so the current socket/HTTP session drops.
|
||||||
|
Only run when you can rejoin the device network afterwards.
|
||||||
|
"""
|
||||||
|
return self.send_rpc(
|
||||||
|
CommunicationCmd.CONNECT_WIFI,
|
||||||
|
operate=CommunicationOperate.SET,
|
||||||
|
data={"ssid": ssid, "password": password,
|
||||||
|
"wifi_SSID": ssid, "wifi_Pw": password},
|
||||||
|
)
|
||||||
|
|
||||||
|
def scan_home_wifi(self) -> Dict[str, Any]:
|
||||||
|
"""``scanWifiList`` — networks visible to the aGate (socket 1109)."""
|
||||||
|
return self.send_rpc(CommunicationCmd.SCAN_WIFI_LIST)
|
||||||
|
|
||||||
|
def query_connection_status(self) -> Dict[str, Any]:
|
||||||
|
"""``connectionStatus`` — current STA association state."""
|
||||||
|
return self.send_rpc(CommunicationCmd.CONNECTION_STATUS)
|
||||||
|
|
||||||
# --------------------------------------------------- business operations
|
# --------------------------------------------------- business operations
|
||||||
# Intention-revealing reads: each runs the app's query sequence for one
|
# Intention-revealing reads: each runs the app's query sequence for one
|
||||||
# question and returns ``{"value", "source", "detail", "reply", ...}``.
|
# question and returns ``{"value", "source", "detail", "reply", ...}``.
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from .battery import _walk
|
from .battery import _walk
|
||||||
from .device_local import LocalDeviceClient
|
from .device_local import LocalDeviceClient
|
||||||
|
from .wifi import DEVICE_GATEWAY_IP
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
HOST = "10.100.1.1"
|
HOST = DEVICE_GATEWAY_IP
|
||||||
PORTS: Tuple[int, ...] = (9000, 18000)
|
PORTS: Tuple[int, ...] = (9000, 18000)
|
||||||
|
|
||||||
#: Reply keys holding watt readings, in priority order.
|
#: Reply keys holding watt readings, in priority order.
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ def direct_connect_request(
|
|||||||
equip_no: str,
|
equip_no: str,
|
||||||
data: Optional[Dict[str, Any]] = None,
|
data: Optional[Dict[str, Any]] = None,
|
||||||
snno: int = 0,
|
snno: int = 0,
|
||||||
|
operate: CommunicationOperate = CommunicationOperate.QUERY,
|
||||||
) -> CommunicationObject:
|
) -> CommunicationObject:
|
||||||
"""Build a *direct-connect* query envelope.
|
"""Build a *direct-connect* query envelope.
|
||||||
|
|
||||||
@@ -190,13 +191,13 @@ def direct_connect_request(
|
|||||||
``sendObject`` (see :class:`SequenceNumber`); pass the issued value
|
``sendObject`` (see :class:`SequenceNumber`); pass the issued value
|
||||||
when re-serialising.
|
when re-serialising.
|
||||||
"""
|
"""
|
||||||
payload: Dict[str, Any] = {"opt": int(CommunicationOperate.QUERY)}
|
payload: Dict[str, Any] = {"opt": int(operate)}
|
||||||
if data:
|
if data:
|
||||||
payload.update(data)
|
payload.update(data)
|
||||||
return CommunicationObject(
|
return CommunicationObject(
|
||||||
cmd_type=socket_cmd_type(cmd),
|
cmd_type=socket_cmd_type(cmd),
|
||||||
equip_no=equip_no,
|
equip_no=equip_no,
|
||||||
op_type=CommunicationOperate.QUERY,
|
op_type=int(operate),
|
||||||
snno=snno,
|
snno=snno,
|
||||||
data=payload,
|
data=payload,
|
||||||
)
|
)
|
||||||
@@ -65,7 +65,8 @@ class HotspotJoinError(RuntimeError):
|
|||||||
"""A join was issued but the association never verified in time."""
|
"""A join was issued but the association never verified in time."""
|
||||||
|
|
||||||
|
|
||||||
def derive_hotspot_credentials(sn: str) -> "HotspotProfile":
|
def derive_hotspot_credentials(sn: str,
|
||||||
|
gateway_ip: Optional[str] = None) -> "HotspotProfile":
|
||||||
"""``getDeviceApSsid`` / ``getDeviceApDefaultPwd``.
|
"""``getDeviceApSsid`` / ``getDeviceApDefaultPwd``.
|
||||||
|
|
||||||
Offline fallback: for a *20-character* SN the SSID is ``"AP_" + sn[11:]``
|
Offline fallback: for a *20-character* SN the SSID is ``"AP_" + sn[11:]``
|
||||||
@@ -74,6 +75,9 @@ def derive_hotspot_credentials(sn: str) -> "HotspotProfile":
|
|||||||
|
|
||||||
The authoritative values come from the live device
|
The authoritative values come from the live device
|
||||||
(``/terminal/getHotSpotInfo`` + ``/terminal/getHosSpotPassword``).
|
(``/terminal/getHotSpotInfo`` + ``/terminal/getHosSpotPassword``).
|
||||||
|
|
||||||
|
``gateway_ip`` overrides the default SoftAP gateway (``10.100.1.1``) —
|
||||||
|
e.g. when the aGate is reached on its home-LAN address instead.
|
||||||
"""
|
"""
|
||||||
if len(sn) == VALID_SN_LENGTH:
|
if len(sn) == VALID_SN_LENGTH:
|
||||||
ssid = "AP_" + sn[11:]
|
ssid = "AP_" + sn[11:]
|
||||||
@@ -83,7 +87,8 @@ def derive_hotspot_credentials(sn: str) -> "HotspotProfile":
|
|||||||
len(sn), VALID_SN_LENGTH)
|
len(sn), VALID_SN_LENGTH)
|
||||||
ssid = "AP_" + sn
|
ssid = "AP_" + sn
|
||||||
password = FALLBACK_PASSWORD
|
password = FALLBACK_PASSWORD
|
||||||
return HotspotProfile(ssid=ssid, password=password)
|
return HotspotProfile(ssid=ssid, password=password,
|
||||||
|
gateway_ip=gateway_ip or DEVICE_GATEWAY_IP)
|
||||||
|
|
||||||
|
|
||||||
class WifiBackend(ABC):
|
class WifiBackend(ABC):
|
||||||
@@ -308,9 +313,14 @@ class WifiManager:
|
|||||||
# -- device hotspot ------------------------------------------------------
|
# -- device hotspot ------------------------------------------------------
|
||||||
def join_device_hotspot(self, sn: str, *, verify: bool = True,
|
def join_device_hotspot(self, sn: str, *, verify: bool = True,
|
||||||
settle_timeout: float = 30.0,
|
settle_timeout: float = 30.0,
|
||||||
poll_interval: float = 1.0) -> HotspotProfile:
|
poll_interval: float = 1.0,
|
||||||
"""Derive the profile from the SN, join, and verify the association."""
|
gateway_ip: Optional[str] = None) -> HotspotProfile:
|
||||||
profile = derive_hotspot_credentials(sn)
|
"""Derive the profile from the SN, join, and verify the association.
|
||||||
|
|
||||||
|
``gateway_ip`` overrides the device address recorded on the
|
||||||
|
profile (default ``10.100.1.1``) without changing the radio join.
|
||||||
|
"""
|
||||||
|
profile = derive_hotspot_credentials(sn, gateway_ip=gateway_ip)
|
||||||
return self._join(profile, verify=verify,
|
return self._join(profile, verify=verify,
|
||||||
settle_timeout=settle_timeout,
|
settle_timeout=settle_timeout,
|
||||||
poll_interval=poll_interval)
|
poll_interval=poll_interval)
|
||||||
|
|||||||
@@ -104,3 +104,30 @@ class TestWattageOp(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
class TestWifiOps(unittest.TestCase):
|
||||||
|
def test_join_home_wifi_shape(self):
|
||||||
|
from franklinwh_agate_client.protocol import direct_connect_request
|
||||||
|
from franklinwh_agate_client.protocol import CommunicationCmd
|
||||||
|
from franklinwh_agate_client.protocol import CommunicationOperate
|
||||||
|
env = direct_connect_request(
|
||||||
|
CommunicationCmd.CONNECT_WIFI, "SN",
|
||||||
|
data={"ssid": "Home", "password": "pw",
|
||||||
|
"wifi_SSID": "Home", "wifi_Pw": "pw"},
|
||||||
|
operate=CommunicationOperate.SET)
|
||||||
|
d = env.to_dict()
|
||||||
|
self.assertEqual(d["cmdType"], 1111) # socket id of connectWifi
|
||||||
|
self.assertEqual(d["type"], 1) # SET
|
||||||
|
self.assertEqual(d["dataArea"]["opt"], 1)
|
||||||
|
self.assertEqual(d["dataArea"]["ssid"], "Home")
|
||||||
|
|
||||||
|
def test_scan_and_status_shape(self):
|
||||||
|
from franklinwh_agate_client.protocol import direct_connect_request
|
||||||
|
from franklinwh_agate_client.protocol import CommunicationCmd
|
||||||
|
scan = direct_connect_request(CommunicationCmd.SCAN_WIFI_LIST, "SN")
|
||||||
|
self.assertEqual(scan.cmd_type, 1109)
|
||||||
|
self.assertEqual(scan.to_dict()["dataArea"], {"opt": 0})
|
||||||
|
status = direct_connect_request(
|
||||||
|
CommunicationCmd.CONNECTION_STATUS, "SN")
|
||||||
|
self.assertEqual(status.cmd_type, 1113)
|
||||||
Reference in new issue
Block a user