Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- added `client.sms.get_by_id()` and `client.sms.save_draft()` from normalized public contracts; draft create/update preserves the historically live-verified wire distinction (string id/type/protocol, boolean gsm7), enforces the save success triple, and redacts message content from SDK-generated errors
### Added

- added read-only `client.lan.static_reservations()` for the upstream live-verified normal-admin `router/router_get_dhcp_static_ip` contract; the complete firmware JSON response is preserved raw because the public API does not freeze a stable nested reservation schema, and the disruptive reservation setter remains outside this block
- added read-only `client.device.work_mode()` for the upstream live-verified normal-admin `router/router_get_work_mode` contract; source-known normal values are `router` and `bridge`, while the SDK preserves raw `mode`/`result` values and deliberately does not expose the disruptive work-mode setter in this block
- added read-only `client.mobile.network_select_mode()` for the upstream live-verified normal-admin `util_wan/get_network_select_mode` contract; the raw `nw_sel_mode`/`result` response is preserved without semantic aliases, and operator scan/manual selection remain intentionally outside this helper
- added read-only `client.ota.updated_status()` and `client.ota.query_state()` wrappers for the upstream live-verified OTA status/state contracts; `query_state()` sends exactly `{"type": 1}`, preserves raw state strings, and does not reinterpret `idle` as proof that firmware is current
Expand Down Expand Up @@ -56,6 +57,7 @@

### Physical validation

- DHCP static-reservation read physically validated on ACIY.3 on 2026-09-08: targeted `test_lan_dns_reads` exercised the existing LAN/DNS reads plus `router/router_get_dhcp_static_ip` through `client.lan.static_reservations()` and passed `1/1` in 0.92 s; no DHCP/LAN write occurred and no concrete reservation IP/MAC values were printed
- router work-mode read physically validated on ACIY.3 on 2026-09-08: targeted `test_device_health_reads` exercised the existing safe device-health reads plus `router/router_get_work_mode` through `client.device.work_mode()` and passed `1/1` in 3.43 s; no work-mode write, bridge transition, reboot or connectivity mutation occurred
- WAN network selection mode read physically validated on ACIY.3 on 2026-09-08: targeted `test_mobile_status_reads` exercised the existing mobile read group plus `util_wan/get_network_select_mode` through `client.mobile.network_select_mode()` and passed `1/1` in 0.61 s; no operator scan, network-selection write or connectivity transition occurred
- OTA read-only namespace physically validated on ACIY.3 on 2026-09-08: targeted `test_ota_reads` exercised `ota/get_updated_status` and `ota/new_query` with exactly `{"type": 1}` through `client.ota` and passed `1/1` in 0.47 s; no manual update check, download, install, state clear or cancellation action was invoked
Expand Down
32 changes: 32 additions & 0 deletions docs/dhcp-static-reservations-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# DHCP static-reservation read coverage

This SDK block exposes only the already live-verified normal-admin read contract `router/router_get_dhcp_static_ip` as `client.lan.static_reservations()`.

## Scope

- SDK helper: `client.lan.static_reservations()`
- API method: `router/router_get_dhcp_static_ip`
- transport: GET, no request body
- the complete firmware JSON response is preserved raw

The upstream public contract intentionally does not freeze a stable nested schema for this response, so the SDK does not invent reservation field names or normalize the returned structure.

The SDK deliberately does **not** expose or exercise `router/router_set_dhcp_static_ip` in this block.

## Privacy

DHCP reservations can contain private LAN addresses and device MAC addresses. The physical smoke validates only that the helper returns a mapping and does not print or assert concrete reservation values.

## Physical evidence — 2026-09-08

Target: Zyxel NR2301, tested firmware family ACIY.3, Python 3.13.5.

The targeted read-only LAN/DNS integration selection exercised the existing LAN/DNS reads plus `client.lan.static_reservations()` and completed successfully:

```text
1 passed, 10 deselected in 0.92 s
```

No DHCP/LAN configuration write occurred, and no reservation IP/MAC values were printed.

No `nr2301-api` change was required by this run because `router/router_get_dhcp_static_ip` was already `LIVE_VERIFIED`, `ADMIN_OK` and `READ_OR_LOW_SIDE_EFFECT`; this adds public-SDK physical-path evidence only.
14 changes: 14 additions & 0 deletions src/nr2301/namespaces/lan.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ def dhcp(self, *, timeout: float | None = None) -> DHCPSettings:
)
return cast(DHCPSettings, dict(self._extract_dhcp(response)))

def static_reservations(self, *, timeout: float | None = None) -> dict[str, Any]:
"""Return the raw DHCP static-reservation response.

The upstream API marks `router_get_dhcp_static_ip` as live-verified but
does not freeze a stable nested response schema. Preserve the complete
firmware JSON object without inventing field names or normalizations.
"""

return self._client.call(
"router",
"router_get_dhcp_static_ip",
timeout=timeout,
)

def dns(self, *, timeout: float | None = None) -> DNSSettings:
"""Return the five DNS fields from the combined DHCP object."""

Expand Down
3 changes: 3 additions & 0 deletions tests/integration/test_readonly_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ def test_mobile_status_reads(router):
def test_lan_dns_reads(router):
_assert_mapping(router.lan.address())
_assert_mapping(router.lan.dns())
# Static DHCP reservations can contain private IP/MAC identifiers. Exercise
# only the read contract and never print or assert concrete reservation data.
_assert_mapping(router.lan.static_reservations())


def test_firewall_reads(router):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_lan_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ def test_lan_read_helpers_use_live_verified_get_methods():
assert session.calls[3][2]["params"]["method"] == "router_get_lan_ip"


def test_static_reservations_uses_live_verified_getter_and_preserves_raw_response():
payload = {
"synthetic_nested_shape": [
{"unknown_future_field": "preserve-me"},
],
"result": 0,
}
client, session = authenticated_client([(payload, 200)])

assert client.lan.static_reservations() == payload

assert len(session.calls) == 1
method, _, kwargs = session.calls[0]
assert method == "GET"
assert kwargs["params"]["path"] == "router"
assert kwargs["params"]["method"] == "router_get_dhcp_static_ip"
assert "json" not in kwargs


def test_set_dns_preserves_combined_settings_and_verifies_readback():
before = dhcp_payload()
after = dhcp_payload(
Expand Down
Loading