From 53197c46d48e2d0325673caad70cf9c17a190952 Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:11:01 +0200 Subject: [PATCH 1/7] sdk: add DHCP reservation read helper --- src/nr2301/namespaces/lan.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/nr2301/namespaces/lan.py b/src/nr2301/namespaces/lan.py index 8780209..9b045c4 100644 --- a/src/nr2301/namespaces/lan.py +++ b/src/nr2301/namespaces/lan.py @@ -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.""" From fd7b4186bfb53b4e80544dee63436b2f090daf8a Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:11:44 +0200 Subject: [PATCH 2/7] tests: cover DHCP reservation read helper --- tests/test_lan_namespace.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_lan_namespace.py b/tests/test_lan_namespace.py index 49ac89d..585f709 100644 --- a/tests/test_lan_namespace.py +++ b/tests/test_lan_namespace.py @@ -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( From 12b44577494efebf370f13ce7900dd71b6d19c66 Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:12:16 +0200 Subject: [PATCH 3/7] tests: add sanitized DHCP reservation read smoke --- tests/integration/test_readonly_router.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/test_readonly_router.py b/tests/integration/test_readonly_router.py index 08e1873..db348d5 100644 --- a/tests/integration/test_readonly_router.py +++ b/tests/integration/test_readonly_router.py @@ -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): From 5234e3bbb9a1196ebc4cd9ba8a624ac2a3259b40 Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:17:53 +0200 Subject: [PATCH 4/7] docs: record DHCP static reservation read evidence --- docs/dhcp-static-reservations-read.md | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/dhcp-static-reservations-read.md diff --git a/docs/dhcp-static-reservations-read.md b/docs/dhcp-static-reservations-read.md new file mode 100644 index 0000000..e916152 --- /dev/null +++ b/docs/dhcp-static-reservations-read.md @@ -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. From eb1477f84041bac9a20dcf3c8b2baa41bee8ee3d Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:20:02 +0200 Subject: [PATCH 5/7] docs: record DHCP static reservation read validation --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12eaf6..cf88a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 @@ -67,7 +69,7 @@ - SIM `provide_pin` lifecycle test passed on 2026-08-31 in 76.02 s: after enabling PIN protection, a real reboot outage was confirmed, administrator login recovered on attempt 27, the SIM stabilized at `pin_status=2`, one known-correct local PIN returned `response.setting_response=OK`, read-back returned to `pin_status=5`, retry counters remained 3/10, and PIN protection was restored to disabled - WPS action integration passed on 2026-08-31 in 1.44 s: PBC returned nested `wireless.wps_call_pbc_result=OK`, Cancel returned flat top-level `wps_call_cancel_result=OK`, PIN `12345670` returned nested `wireless.wps_call_pin_result=OK`, the second Cancel was again flat/OK, and the original WPS-enable state was restored - complete Wi-Fi security matrix finished on 2026-08-31: 52/52 section/token combinations accepted; every protected mode round-tripped the synthetic key on all four AP sections, open mode exposed section-specific key-field behavior, and `password_modified` remained 0 throughout -- extended Wi-Fi capability suite passed on 2026-08-31: all 18 cases passed in 226.96 s, confirming raw `power_level` values 0/1/2, global and Guest maxassoc=1, Guest 2.4G/5G band mode, synthetic SSID writes on all four AP blocks, 2.4-GHz channel 13, 5-GHz channels 52/100/140 including DFS-class paths, every source-known WebUI net-mode/bandwidth token, and normal-admin `wifi_scan`, with original state restored after every mutation +- extended Wi-Fi capability suite passed on 2026-08-31: all 18 cases passed in 226.96 s, confirming raw `power_level` values 0/1/2, global and Guest max-client lower-bound probes, Guest 2.4G/5G band mode, synthetic SSID writes on all four AP blocks, 2.4-GHz channel 13, 5-GHz channels 52/100/140 including DFS-class paths, every source-known WebUI net-mode/bandwidth token, and normal-admin `wifi_scan`, with original state restored after every mutation - comprehensive Wi-Fi field suite passed on 2026-08-31: all 15 cases passed in 178.96 s, covering representative 2.4/5-GHz fixed channels, Hidden on 24G/5G/DUAL/Guest, AP isolation on both bands, global maxassoc, timed-off persistence, master Wi-Fi switch, per-band net modes and bandwidths, with exact read-back and final original-state restoration - combined LAN/DHCP/DNS physical write test passed on 2026-08-31: DNS-only mutation preserved all seven non-DNS fields and the complete original 12-field object was restored exactly - first full read-only physical SDK smoke completed successfully on 2026-08-31 against the USB-connected NR2301 using Python 3.13.5 and `http://zyxel.home`: all 8 integration groups passed in 4.01 s (version, device health, SIM, mobile, LAN/DNS reads, Wi-Fi status, SMS summary and statistics) From bee7576a9e34b3c5915cb8336637ae755ae3dad0 Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:21:27 +0200 Subject: [PATCH 6/7] docs: preserve historical Wi-Fi validation wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf88a00..0c5d639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,7 +69,7 @@ - SIM `provide_pin` lifecycle test passed on 2026-08-31 in 76.02 s: after enabling PIN protection, a real reboot outage was confirmed, administrator login recovered on attempt 27, the SIM stabilized at `pin_status=2`, one known-correct local PIN returned `response.setting_response=OK`, read-back returned to `pin_status=5`, retry counters remained 3/10, and PIN protection was restored to disabled - WPS action integration passed on 2026-08-31 in 1.44 s: PBC returned nested `wireless.wps_call_pbc_result=OK`, Cancel returned flat top-level `wps_call_cancel_result=OK`, PIN `12345670` returned nested `wireless.wps_call_pin_result=OK`, the second Cancel was again flat/OK, and the original WPS-enable state was restored - complete Wi-Fi security matrix finished on 2026-08-31: 52/52 section/token combinations accepted; every protected mode round-tripped the synthetic key on all four AP sections, open mode exposed section-specific key-field behavior, and `password_modified` remained 0 throughout -- extended Wi-Fi capability suite passed on 2026-08-31: all 18 cases passed in 226.96 s, confirming raw `power_level` values 0/1/2, global and Guest max-client lower-bound probes, Guest 2.4G/5G band mode, synthetic SSID writes on all four AP blocks, 2.4-GHz channel 13, 5-GHz channels 52/100/140 including DFS-class paths, every source-known WebUI net-mode/bandwidth token, and normal-admin `wifi_scan`, with original state restored after every mutation +- extended Wi-Fi capability suite passed on 2026-08-31: all 18 cases passed in 226.96 s, confirming raw `power_level` values 0/1/2, global and Guest maxassoc=1, Guest 2.4G/5G band mode, synthetic SSID writes on all four AP blocks, 2.4-GHz channel 13, 5-GHz channels 52/100/140 including DFS-class paths, every source-known WebUI net-mode/bandwidth token, and normal-admin `wifi_scan`, with original state restored after every mutation - comprehensive Wi-Fi field suite passed on 2026-08-31: all 15 cases passed in 178.96 s, covering representative 2.4/5-GHz fixed channels, Hidden on 24G/5G/DUAL/Guest, AP isolation on both bands, global maxassoc, timed-off persistence, master Wi-Fi switch, per-band net modes and bandwidths, with exact read-back and final original-state restoration - combined LAN/DHCP/DNS physical write test passed on 2026-08-31: DNS-only mutation preserved all seven non-DNS fields and the complete original 12-field object was restored exactly - first full read-only physical SDK smoke completed successfully on 2026-08-31 against the USB-connected NR2301 using Python 3.13.5 and `http://zyxel.home`: all 8 integration groups passed in 4.01 s (version, device health, SIM, mobile, LAN/DNS reads, Wi-Fi status, SMS summary and statistics) From 9cd8bd82a61ba0f15343bd4ac89c01a4e93012fd Mon Sep 17 00:00:00 2001 From: Marc Leinen <34336531+MarcLeinenDE@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:22:50 +0200 Subject: [PATCH 7/7] docs: preserve historical Wi-Fi validation wording