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: 1 addition & 1 deletion tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ async def test_reset_network_info(app):
app.form_network = AsyncMock()
await app.reset_network_info()

app.form_network.assert_called_once()
app.form_network.assert_called_once_with(fast=True)


async def test_energy_scan_conbee_2(app):
Expand Down
109 changes: 103 additions & 6 deletions tests/test_network_state.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Test `load_network_info` and `write_network_info` methods."""

import importlib.metadata
from unittest.mock import ANY, call

import pytest
from zigpy.exceptions import ControllerException, NetworkNotFormed
from zigpy.exceptions import CannotWriteNetworkSettings, NetworkNotFormed
import zigpy.state as app_state
import zigpy.types as t
import zigpy.zdo.types as zdo_t
Expand Down Expand Up @@ -70,37 +71,74 @@ def network_info(node_info):
@patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001)
@patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001)
@pytest.mark.parametrize(
"channel_mask, channel, security_level, fw_supports_fc, logical_type",
(
"channel_mask",
"channel",
"security_level",
"fw_supports_fc",
"logical_type",
"tx_counter",
"should_error",
),
[
# FW supports frame counter
(
t.Channels.from_channel_list([15]),
15,
0,
True,
zdo_t.LogicalType.Coordinator,
39009277,
False,
),
# FW doesn't support but we're writing blank settings (tx_counter == 0)
(
t.Channels.from_channel_list([15]),
15,
5,
False,
zdo_t.LogicalType.Coordinator,
0,
False,
),
# FW doesn't support and we're writing real settings (should error)
(
t.Channels.from_channel_list([15]),
15,
0,
False,
zdo_t.LogicalType.Coordinator,
39009277,
True,
),
# Additional test cases with FW support
(
t.Channels.from_channel_list([15, 20]),
15,
5,
True,
zdo_t.LogicalType.Coordinator,
39009277,
False,
),
(
t.Channels.from_channel_list([15, 20, 25]),
None,
5,
True,
zdo_t.LogicalType.Router,
39009277,
False,
),
(
None,
15,
5,
True,
zdo_t.LogicalType.Coordinator,
39009277,
False,
),
(None, 15, 5, True, zdo_t.LogicalType.Coordinator),
],
)
async def test_write_network_info(
Expand All @@ -112,6 +150,8 @@ async def test_write_network_info(
security_level,
fw_supports_fc,
logical_type,
tx_counter,
should_error,
):
"""Test that network info is correctly written."""

Expand All @@ -137,13 +177,14 @@ async def write_parameter(param, *args):
channel=channel,
channel_mask=channel_mask,
security_level=security_level,
network_key=network_info.network_key.replace(tx_counter=tx_counter),
)

node_info = node_info.replace(logical_type=logical_type)

if not fw_supports_fc:
if should_error:
with pytest.raises(
ControllerException,
CannotWriteNetworkSettings,
match=(
"Please upgrade your adapter firmware. Firmware version 0x26580700 does"
" not support writing the network key frame counter, which is required"
Expand All @@ -166,7 +207,9 @@ async def write_parameter(param, *args):
for call in app._api.write_parameter.await_args_list
}

assert params["nwk_frame_counter"] == (network_info.network_key.tx_counter,)
# Only check frame counter if firmware supports it
if fw_supports_fc:
assert params["nwk_frame_counter"] == (network_info.network_key.tx_counter,)

if node_info.logical_type == zdo_t.LogicalType.Coordinator:
assert params["aps_designed_coordinator"] == (1,)
Expand Down Expand Up @@ -328,3 +371,57 @@ async def read_param(param, *args):
assert app.state.network_info == network_info

assert app.state.node_info == node_info.replace(**node_state_changes)


@patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001)
@patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001)
async def test_form_network_fast_without_frame_counter_support(app): # noqa: F811
"""Test that form_network(fast=True) works when FW doesn't support frame counter."""

async def write_parameter(param, *args):
if param == zigpy_deconz.api.NetworkParameter.nwk_frame_counter:
raise zigpy_deconz.exception.CommandError(
"Command is unsupported",
status=zigpy_deconz.api.Status.UNSUPPORTED,
command=None,
)

app._change_network_state = AsyncMock()
app._api.write_parameter = AsyncMock(side_effect=write_parameter)
app.backups = AsyncMock()
app.backups.restore_backup = AsyncMock()

# This should not raise an error because fast=True sets form_quickly
await app.form_network(fast=True)

# Verify that restore_backup was called with create_new=False (due to fast=True)
assert app.backups.restore_backup.mock_calls == [
call(backup=ANY, counter_increment=0, allow_incomplete=True, create_new=False)
]


@patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001)
@patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001)
async def test_reset_network_info_without_frame_counter_support(app): # noqa: F811
"""Test that reset_network_info works even when FW doesn't support frame counter."""

async def write_parameter(param, *args):
if param == zigpy_deconz.api.NetworkParameter.nwk_frame_counter:
raise zigpy_deconz.exception.CommandError(
"Command is unsupported",
status=zigpy_deconz.api.Status.UNSUPPORTED,
command=None,
)

app._change_network_state = AsyncMock()
app._api.write_parameter = AsyncMock(side_effect=write_parameter)
app.backups = AsyncMock()
app.backups.restore_backup = AsyncMock()

# Should not raise an error because reset_network_info calls form_network(fast=True)
await app.reset_network_info()

# Verify that restore_backup was called once (via form_network)
assert app.backups.restore_backup.mock_calls == [
call(backup=ANY, counter_increment=0, allow_incomplete=True, create_new=False)
]
21 changes: 14 additions & 7 deletions zigpy_deconz/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ async def change_loop():

async def reset_network_info(self):
# TODO: There does not appear to be a way to factory reset a Conbee
await self.form_network()
await self.form_network(fast=True)

async def write_network_info(self, *, network_info, node_info):
try:
Expand All @@ -189,12 +189,19 @@ async def write_network_info(self, *, network_info, node_info):
)
except zigpy_deconz.exception.CommandError as ex:
assert ex.status == Status.UNSUPPORTED
fw_version = f"{int(self._api.firmware_version):#010x}"
raise zigpy.exceptions.ControllerException(
f"Please upgrade your adapter firmware. Firmware version {fw_version}"
f" does not support writing the network key frame counter, which is"
f" required for migration to succeed."
)

# If we are resetting the adapter or forming a brand new network, we can
# skip this check
if not (
network_info.stack_specific.get("form_quickly", False)
or network_info.network_key.tx_counter == 0
):
fw_version = f"{int(self._api.firmware_version):#010x}"
raise zigpy.exceptions.CannotWriteNetworkSettings(
f"Please upgrade your adapter firmware. Firmware version"
f" {fw_version} does not support writing the network key frame"
f" counter, which is required for migration to succeed."
)

if node_info.logical_type == zdo_t.LogicalType.Coordinator:
await self._api.write_parameter(
Expand Down
Loading