Skip to content

Commit 07d7cf0

Browse files
authored
Implement can_write_network_settings API (#687)
* Implement `can_write_network_settings` API * Fix unit tests * Bump minimum zigpy version * Modify whitespace to trigger GitHub actions cache bust * Fix unit tests to account for new zigpy packet priority * Add some tests
1 parent 9bb0cf7 commit 07d7cf0

File tree

4 files changed

+158
-31
lines changed

4 files changed

+158
-31
lines changed

bellows/zigbee/application.py

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
import zigpy.config
2020
import zigpy.device
2121
import zigpy.endpoint
22-
from zigpy.exceptions import NetworkNotFormed
22+
from zigpy.exceptions import (
23+
CannotWriteNetworkSettings,
24+
DestructiveWriteNetworkSettings,
25+
NetworkNotFormed,
26+
)
2327
import zigpy.state
2428
import zigpy.types
2529
import zigpy.util
@@ -346,49 +350,77 @@ async def load_network_info(self, *, load_devices=False) -> None:
346350
async for nwk, eui64 in ezsp.read_address_table():
347351
self.state.network_info.nwk_addresses[eui64] = nwk
348352

353+
async def can_write_network_settings(
354+
self,
355+
*,
356+
network_info: zigpy.state.NetworkInfo,
357+
node_info: zigpy.state.NodeInfo,
358+
) -> bool:
359+
# If we are not overwriting the EUI64, we can always write the network info
360+
if node_info.ieee == zigpy.types.EUI64.UNKNOWN:
361+
return True
362+
363+
ezsp = self._ezsp
364+
stack_specific = network_info.stack_specific.get("ezsp", {})
365+
(current_eui64,) = await ezsp.getEui64()
366+
367+
# If we are not actually replacing the EUI64, we can write the network info
368+
if node_info.ieee == current_eui64:
369+
return True
370+
371+
# If the adapter supports EUI64 rewriting, we can always write the network info
372+
if await ezsp.can_rewrite_custom_eui64():
373+
return True
374+
375+
# If the adapter does not support EUI64 rewriting, we cannot proceed
376+
if not await ezsp.can_burn_userdata_custom_eui64():
377+
_, _, version = await self._get_board_info()
378+
raise CannotWriteNetworkSettings(
379+
f"Please upgrade your adapter firmware. The adapter IEEE address"
380+
f" has been overwritten and firmware {version!r} does not support"
381+
f" writing it a second time."
382+
)
383+
384+
# Otherwise, we need explicit confirmation
385+
if not stack_specific.get(
386+
"i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it"
387+
):
388+
_, _, version = await self._get_board_info()
389+
raise DestructiveWriteNetworkSettings(
390+
f"Please upgrade your adapter firmware. The adapter IEEE address"
391+
f" needs to be replaced and firmware {version!r} does not support"
392+
f" writing it multiple times."
393+
)
394+
395+
return True
396+
349397
async def write_network_info(
350398
self, *, network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo
351399
) -> None:
352400
ezsp = self._ezsp
353401

402+
# Throw an error early if we can't actually restore
403+
await self.can_write_network_settings(
404+
network_info=network_info, node_info=node_info
405+
)
354406
await self.reset_network_info()
355407

356408
stack_specific = network_info.stack_specific.get("ezsp", {})
357409
(current_eui64,) = await ezsp.getEui64()
358-
wrote_eui64 = False
359410

360411
if (
361412
node_info.ieee != zigpy.types.EUI64.UNKNOWN
362413
and node_info.ieee != current_eui64
363414
):
364415
if await ezsp.can_rewrite_custom_eui64():
365416
await ezsp.write_custom_eui64(node_info.ieee)
366-
wrote_eui64 = True
367-
elif not stack_specific.get(
368-
"i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it"
369-
):
370-
_, _, version = await self._get_board_info()
371-
raise ControllerError(
372-
f"Please upgrade your adapter firmware. The adapter IEEE address"
373-
f" needs to be replaced and firmware {version!r} does not support"
374-
f" writing it multiple times."
375-
)
376-
elif not await ezsp.can_burn_userdata_custom_eui64():
377-
_, _, version = await self._get_board_info()
378-
raise ControllerError(
379-
f"Please upgrade your adapter firmware. The adapter IEEE address"
380-
f" has been overwritten and firmware {version!r} does not support"
381-
f" writing it a second time."
382-
)
383417
else:
418+
assert await ezsp.can_burn_userdata_custom_eui64()
384419
await ezsp.write_custom_eui64(node_info.ieee, burn_into_userdata=True)
385-
wrote_eui64 = True
386420

387-
if wrote_eui64:
388-
# Reset after writing the EUI64, as it touches NVRAM
389421
await self._reset()
390422
else:
391-
# If we cannot write the new EUI64, don't mess up key entries with the
423+
# If we do not write the new EUI64, don't mess up key entries with the
392424
# unwritten EUI64 address
393425
node_info.ieee = current_eui64
394426
network_info.tc_link_key.partner_ieee = current_eui64

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies = [
1717
"click",
1818
"click-log>=0.2.1",
1919
"voluptuous",
20-
"zigpy>=0.79.0",
20+
"zigpy>=0.83.0",
2121
'async-timeout; python_version<"3.11"',
2222
]
2323

requirements_test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ pytest-asyncio>=0.17
1717
pytest>=7.1.3
1818
zigpy>=0.54.1
1919
ruff==0.0.261
20-
Flake8-pyproject
20+
Flake8-pyproject

tests/test_application.py

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ def send_unicast(aps_frame, data, message_tag, nwk):
756756
)
757757

758758
await app.send_packet(packet)
759-
app._concurrent_requests_semaphore.max_value = 10000
759+
app._concurrent_requests_semaphore.max_concurrency = 10000
760760
results = await asyncio.gather(
761761
*(app.send_packet(packet) for _ in range(256 + 1)), return_exceptions=True
762762
)
@@ -1024,7 +1024,7 @@ async def test_send_packet_unicast_concurrency(app, packet, monkeypatch):
10241024
monkeypatch.setattr(bellows.zigbee.application, "MESSAGE_SEND_TIMEOUT_MAINS", 0.5)
10251025
monkeypatch.setattr(bellows.zigbee.application, "MESSAGE_SEND_TIMEOUT_BATTERY", 0.5)
10261026

1027-
app._concurrent_requests_semaphore.max_value = 10
1027+
app._concurrent_requests_semaphore.max_concurrency = 12
10281028

10291029
max_concurrency = 0
10301030
in_flight_requests = 0
@@ -1073,9 +1073,14 @@ async def send_unicast(nwk, aps_frame, message_tag, data):
10731073

10741074
app._ezsp.send_unicast = AsyncMock(side_effect=send_unicast)
10751075

1076-
responses = await asyncio.gather(*[app.send_packet(packet) for _ in range(100)])
1076+
responses = await asyncio.gather(
1077+
*[
1078+
app.send_packet(packet.replace(priority=zigpy_t.PacketPriority.HIGH))
1079+
for _ in range(100)
1080+
]
1081+
)
10771082
assert len(responses) == 100
1078-
assert max_concurrency == 10
1083+
assert max_concurrency == 12
10791084
assert in_flight_requests == 0
10801085

10811086

@@ -2027,6 +2032,96 @@ async def test_write_network_info(
20272032
]
20282033

20292034

2035+
@pytest.mark.parametrize(
2036+
("node_ieee", "current_eui64", "can_rewrite", "can_burn", "confirmation_flag"),
2037+
[
2038+
(
2039+
zigpy_t.EUI64.UNKNOWN,
2040+
t.EUI64.convert("00:01:02:03:04:05:06:07"),
2041+
False,
2042+
False,
2043+
False,
2044+
),
2045+
(
2046+
t.EUI64.convert("00:01:02:03:04:05:06:07"),
2047+
t.EUI64.convert("00:01:02:03:04:05:06:07"),
2048+
False,
2049+
False,
2050+
False,
2051+
),
2052+
(
2053+
t.EUI64.convert("aa:aa:aa:aa:aa:aa:aa:aa"),
2054+
t.EUI64.convert("00:01:02:03:04:05:06:07"),
2055+
True,
2056+
False,
2057+
False,
2058+
),
2059+
(
2060+
t.EUI64.convert("aa:aa:aa:aa:aa:aa:aa:aa"),
2061+
t.EUI64.convert("00:01:02:03:04:05:06:07"),
2062+
False,
2063+
True,
2064+
True,
2065+
),
2066+
],
2067+
)
2068+
async def test_can_write_network_settings(
2069+
app: ControllerApplication,
2070+
zigpy_backup: zigpy.backups.NetworkBackup,
2071+
node_ieee: t.EUI64,
2072+
current_eui64: t.EUI64,
2073+
can_rewrite: bool,
2074+
can_burn: bool,
2075+
confirmation_flag: bool,
2076+
) -> None:
2077+
"""Test `can_write_network_settings`."""
2078+
app._ezsp.getEui64 = AsyncMock(return_value=[current_eui64])
2079+
app._ezsp.can_rewrite_custom_eui64 = AsyncMock(return_value=can_rewrite)
2080+
app._ezsp.can_burn_userdata_custom_eui64 = AsyncMock(return_value=can_burn)
2081+
app._get_board_info = AsyncMock(
2082+
return_value=("Mock board", "Mock Manufacturer", "Mock version")
2083+
)
2084+
2085+
node_info = zigpy_backup.node_info.replace(ieee=node_ieee)
2086+
network_info = zigpy_backup.network_info
2087+
2088+
if confirmation_flag:
2089+
network_info = network_info.replace(
2090+
stack_specific={
2091+
"ezsp": {
2092+
**network_info.stack_specific.get("ezsp", {}),
2093+
"i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True,
2094+
}
2095+
}
2096+
)
2097+
2098+
assert await app.can_write_network_settings(
2099+
network_info=network_info,
2100+
node_info=node_info,
2101+
)
2102+
2103+
2104+
async def test_write_network_info_uses_write_custom_eui64(
2105+
app: ControllerApplication,
2106+
zigpy_backup: zigpy.backups.NetworkBackup,
2107+
) -> None:
2108+
"""Test that `write_network_info` uses `write_custom_eui64` correctly."""
2109+
app._ezsp.can_rewrite_custom_eui64 = AsyncMock(return_value=True)
2110+
app._ezsp.write_custom_eui64 = AsyncMock()
2111+
2112+
different_ieee = t.EUI64.convert("aa:aa:aa:aa:aa:aa:aa:aa")
2113+
node_info = zigpy_backup.node_info.replace(ieee=different_ieee)
2114+
2115+
with patch.object(app, "_reset"):
2116+
await app.write_network_info(
2117+
node_info=node_info,
2118+
network_info=zigpy_backup.network_info,
2119+
)
2120+
2121+
# Verify write_custom_eui64 was called without burn_into_userdata flag
2122+
assert app._ezsp.write_custom_eui64.mock_calls == [call(different_ieee)]
2123+
2124+
20302125
async def test_network_scan(app: ControllerApplication) -> None:
20312126
app._ezsp._protocol.startScan.return_value = [t.sl_Status.OK]
20322127

@@ -2209,7 +2304,7 @@ async def test_migration_failure_eui64_overwrite_confirmation(
22092304
# Migration explicitly fails if we need to write the EUI64 but the adapter treats it
22102305
# as a write-once operation
22112306
with pytest.raises(
2212-
zigpy.exceptions.ControllerException,
2307+
zigpy.exceptions.DestructiveWriteNetworkSettings,
22132308
match=(
22142309
"Please upgrade your adapter firmware. The adapter IEEE address needs to be"
22152310
" replaced and firmware 'Mock version' does not support writing it multiple"
@@ -2229,7 +2324,7 @@ async def test_migration_failure_eui64_overwrite_confirmation(
22292324
app._ezsp, "write_custom_eui64", wraps=app._ezsp.write_custom_eui64
22302325
), patch.object(app._ezsp, "can_burn_userdata_custom_eui64", return_value=False):
22312326
with pytest.raises(
2232-
zigpy.exceptions.ControllerException,
2327+
zigpy.exceptions.CannotWriteNetworkSettings,
22332328
match=(
22342329
"Please upgrade your adapter firmware. The adapter IEEE address has"
22352330
" been overwritten and firmware 'Mock version' does not support writing"

0 commit comments

Comments
 (0)