diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9776b058..543131ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: flake8 entry: pflake8 additional_dependencies: - - pyproject-flake8==0.0.1a2 + - pyproject-flake8==0.0.1a3 - flake8-bugbear==22.1.11 - flake8-comprehensions==3.8.0 - flake8_2020==1.6.1 @@ -29,11 +29,11 @@ repos: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.931 + rev: v0.942 hooks: - id: mypy additional_dependencies: - - zigpy==0.43.0 + - zigpy - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 @@ -43,4 +43,4 @@ repos: - repo: https://github.com/fsouza/autoflake8 rev: v0.3.1 hooks: - - id: autoflake8 \ No newline at end of file + - id: autoflake8 diff --git a/setup.cfg b/setup.cfg index 7b850549..48b1de63 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ python_requires = >=3.7 install_requires = pyserial-asyncio; platform_system!="Windows" pyserial-asyncio!=0.5; platform_system=="Windows" # 0.5 broke writes - zigpy>=0.40.0 + zigpy>=0.47.0 async_timeout voluptuous coloredlogs diff --git a/tests/api/test_network_state.py b/tests/api/test_network_state.py index a14c7ec6..f89c2e01 100644 --- a/tests/api/test_network_state.py +++ b/tests/api/test_network_state.py @@ -1,8 +1,8 @@ import logging -import dataclasses import pytest +import zigpy_znp.types as t from zigpy_znp.types.nvids import ExNvIds, OsalNvIds from ..conftest import ( @@ -31,16 +31,22 @@ async def test_state_transfer(from_device, to_device, make_connected_znp): # Z-Stack 1 devices can't have some security info read out if issubclass(from_device, BaseZStack1CC2531): - assert formed_znp.network_info == dataclasses.replace( - empty_znp.network_info, stack_specific={} + assert formed_znp.network_info == empty_znp.network_info.replace( + stack_specific={}, + metadata=formed_znp.network_info.metadata, ) elif issubclass(to_device, BaseZStack1CC2531): assert ( - dataclasses.replace(formed_znp.network_info, stack_specific={}) + formed_znp.network_info.replace( + stack_specific={}, + metadata=empty_znp.network_info.metadata, + ) == empty_znp.network_info ) else: - assert formed_znp.network_info == empty_znp.network_info + assert formed_znp.network_info == empty_znp.network_info.replace( + metadata=formed_znp.network_info.metadata + ) assert formed_znp.node_info == empty_znp.node_info @@ -59,3 +65,30 @@ async def test_broken_cc2531_load_state(device, make_connected_znp, caplog): assert "inconsistent" in caplog.text znp.close() + + +@pytest.mark.parametrize("device", [FormedZStack3CC2531]) +async def test_state_write_tclk_zstack3(device, make_connected_znp, caplog): + formed_znp, _ = await make_connected_znp(server_cls=device) + + await formed_znp.load_network_info() + formed_znp.close() + + empty_znp, _ = await make_connected_znp(server_cls=device) + + caplog.set_level(logging.WARNING) + await empty_znp.write_network_info( + network_info=formed_znp.network_info.replace( + tc_link_key=formed_znp.network_info.tc_link_key.replace( + # Non-standard TCLK + key=t.KeyData.convert("AA:BB:CC:DD:AA:BB:CC:DD:AA:BB:CC:DD:AA:BB:CC:DD") + ) + ), + node_info=formed_znp.node_info, + ) + assert "TC link key is configured at build time in Z-Stack 3" in caplog.text + + await empty_znp.load_network_info() + + # TCLK was not changed + assert formed_znp.network_info == empty_znp.network_info diff --git a/tests/application/test_nvram_migration.py b/tests/api/test_nvram_migration.py similarity index 91% rename from tests/application/test_nvram_migration.py rename to tests/api/test_nvram_migration.py index 421d92d1..78456803 100644 --- a/tests/application/test_nvram_migration.py +++ b/tests/api/test_nvram_migration.py @@ -44,7 +44,7 @@ async def test_addrmgr_empty_entries(make_connected_znp, device): @pytest.mark.parametrize("device", [FormedZStack3CC2531]) -async def test_addrmgr_rewrite_fix(device, make_application, mocker): +async def test_addrmgr_rewrite_fix(device, make_connected_znp): # Keep track of reads addrmgr_reads = [] @@ -60,7 +60,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker): extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"), ) - app, znp_server = make_application(server_cls=device) + znp, znp_server = await make_connected_znp(server_cls=device) znp_server.callback_for_response( c.SYS.OSALNVReadExt.Req(Id=OsalNvIds.ADDRMGR, Offset=0), addrmgr_reads.append ) @@ -81,8 +81,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker): assert old_addrmgr != nvram[OsalNvIds.ADDRMGR] assert len(addrmgr_reads) == 0 - await app.startup() - await app.shutdown() + await znp.migrate_nvram() assert len(addrmgr_reads) == 2 # Bad entries have been fixed @@ -94,8 +93,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker): # Will not be read again assert len(addrmgr_reads) == 2 - await app.startup() - await app.shutdown() + await znp.migrate_nvram() assert len(addrmgr_reads) == 2 # Will be migrated again if the migration NVID is deleted @@ -104,8 +102,7 @@ async def test_addrmgr_rewrite_fix(device, make_application, mocker): old_addrmgr2 = nvram[OsalNvIds.ADDRMGR] assert len(addrmgr_reads) == 2 - await app.startup() - await app.shutdown() + await znp.migrate_nvram() assert len(addrmgr_reads) == 3 # But nothing will change diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index 6568dad4..60d6e1e9 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -1,4 +1,5 @@ import asyncio +from unittest.mock import patch import pytest @@ -6,7 +7,7 @@ from zigpy_znp.uart import connect as uart_connect from zigpy_znp.zigbee.application import ControllerApplication -from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1, swap_attribute +from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1 async def test_no_double_connect(make_znp_server, mocker): @@ -57,7 +58,7 @@ async def test_probe_unsuccessful(): @pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_probe_unsuccessful_slow(device, make_znp_server, mocker): +async def test_probe_unsuccessful_slow1(device, make_znp_server, mocker): znp_server = make_znp_server(server_cls=device, shorten_delays=False) # Don't respond to anything @@ -74,6 +75,24 @@ async def test_probe_unsuccessful_slow(device, make_znp_server, mocker): assert not any([t._is_connected for t in znp_server._transports]) +@pytest.mark.parametrize("device", FORMED_DEVICES) +async def test_probe_unsuccessful_slow2(device, make_znp_server, mocker): + znp_server = make_znp_server(server_cls=device, shorten_delays=False) + + # Don't respond to anything + znp_server._listeners.clear() + + mocker.patch("zigpy_znp.zigbee.application.PROBE_TIMEOUT", new=0.1) + + assert not ( + await ControllerApplication.probe( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: znp_server.serial_port}) + ) + ) + + assert not any([t._is_connected for t in znp_server._transports]) + + @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_probe_successful(device, make_znp_server): znp_server = make_znp_server(server_cls=device, shorten_delays=False) @@ -100,8 +119,8 @@ async def test_probe_multiple(device, make_znp_server): @pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_reconnect(device, event_loop, make_application): - app, znp_server = make_application( +async def test_reconnect(device, make_application): + app, znp_server = await make_application( server_cls=device, client_config={ # Make auto-reconnection happen really fast @@ -118,7 +137,7 @@ async def test_reconnect(device, event_loop, make_application): assert app._znp is not None # Don't reply to anything for a bit - with swap_attribute(znp_server, "frame_received", lambda _: None): + with patch.object(znp_server, "frame_received", lambda _: None): # Now that we're connected, have the server close the connection znp_server._uart._transport.close() @@ -143,7 +162,7 @@ async def test_reconnect(device, event_loop, make_application): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_shutdown_from_app(device, mocker, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -159,7 +178,7 @@ async def test_shutdown_from_app(device, mocker, make_application): async def test_clean_shutdown(make_application): - app, znp_server = make_application(server_cls=FormedLaunchpadCC26X2R1) + app, znp_server = await make_application(server_cls=FormedLaunchpadCC26X2R1) await app.startup(auto_form=False) # This should not throw @@ -170,7 +189,7 @@ async def test_clean_shutdown(make_application): async def test_multiple_shutdown(make_application): - app, znp_server = make_application(server_cls=FormedLaunchpadCC26X2R1) + app, znp_server = await make_application(server_cls=FormedLaunchpadCC26X2R1) await app.startup(auto_form=False) await app.shutdown() @@ -179,10 +198,10 @@ async def test_multiple_shutdown(make_application): @pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_reconnect_lockup(device, event_loop, make_application, mocker): +async def test_reconnect_lockup(device, make_application, mocker): mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1) - app, znp_server = make_application( + app, znp_server = await make_application( server_cls=device, client_config={ # Make auto-reconnection happen really fast @@ -197,7 +216,7 @@ async def test_reconnect_lockup(device, event_loop, make_application, mocker): await app.startup(auto_form=False) # Stop responding - with swap_attribute(znp_server, "frame_received", lambda _: None): + with patch.object(znp_server, "frame_received", lambda _: None): assert app._znp is not None assert app._reconnect_task.done() @@ -219,15 +238,16 @@ async def test_reconnect_lockup(device, event_loop, make_application, mocker): await app.shutdown() -@pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_reconnect_lockup_pyserial(device, event_loop, make_application, mocker): +@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +async def test_reconnect_lockup_pyserial(device, make_application, mocker): mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1) - app, znp_server = make_application( + app, znp_server = await make_application( server_cls=device, client_config={ conf.CONF_ZNP_CONFIG: { - conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.1, + conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01, + conf.CONF_SREQ_TIMEOUT: 0.1, } }, ) @@ -242,20 +262,20 @@ async def test_reconnect_lockup_pyserial(device, event_loop, make_application, m # We are connected assert app._znp is not None - did_load_info = asyncio.get_running_loop().create_future() + did_start_network = asyncio.get_running_loop().create_future() - async def patched_load_network_info(*, old_load=app.load_network_info): + async def patched_start_network(old_start_network=app.start_network, **kwargs): try: - return await old_load() + return await old_start_network(**kwargs) finally: - did_load_info.set_result(True) + did_start_network.set_result(True) - with swap_attribute(app, "load_network_info", patched_load_network_info): + with patch.object(app, "start_network", patched_start_network): # "Drop" the connection like PySerial app._znp._uart.connection_lost(exc=None) # Wait until we are reconnecting - await did_load_info + await did_start_network # "Drop" the connection like PySerial again, but during connect app._znp._uart.connection_lost(exc=None) @@ -269,3 +289,49 @@ async def patched_load_network_info(*, old_load=app.load_network_info): assert app._znp and app._znp._uart await app.shutdown() + + +@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +async def test_disconnect(device, make_application): + app, znp_server = await make_application( + server_cls=device, + client_config={ + conf.CONF_ZNP_CONFIG: { + conf.CONF_SREQ_TIMEOUT: 0.1, + } + }, + ) + + assert app._znp is None + await app.connect() + + assert app._znp is not None + + await app.disconnect() + assert app._znp is None + + await app.disconnect() + await app.disconnect() + + +@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +async def test_disconnect_failure(device, make_application): + app, znp_server = await make_application( + server_cls=device, + client_config={ + conf.CONF_ZNP_CONFIG: { + conf.CONF_SREQ_TIMEOUT: 0.1, + } + }, + ) + + assert app._znp is None + await app.connect() + + assert app._znp is not None + + with patch.object(app._znp, "reset", side_effect=RuntimeError("An error")): + # Runs without error + await app.disconnect() + + assert app._znp is None diff --git a/tests/application/test_joining.py b/tests/application/test_joining.py index ca2fff1e..6b9ff9cc 100644 --- a/tests/application/test_joining.py +++ b/tests/application/test_joining.py @@ -21,7 +21,7 @@ @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_permit_join(device, mocker, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) permit_join_coordinator = znp_server.reply_once_to( request=c.ZDO.MgmtPermitJoinReq.Req( @@ -74,7 +74,7 @@ async def test_permit_join(device, mocker, make_application): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_join_coordinator(device, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) # Handle us opening joins on the coordinator permit_join_coordinator = znp_server.reply_once_to( @@ -88,7 +88,7 @@ async def test_join_coordinator(device, make_application): ) await app.startup(auto_form=False) - await app.permit(node=app.ieee) + await app.permit(node=app.state.node_info.ieee) await permit_join_coordinator @@ -100,7 +100,7 @@ async def test_join_device(device, make_application): ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") nwk = 0x1234 - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) device = app.add_initialized_device(ieee=ieee, nwk=nwk) permit_join = znp_server.reply_once_to( @@ -133,7 +133,7 @@ async def test_join_device(device, make_application): @pytest.mark.parametrize("device", FORMED_ZSTACK3_DEVICES) @pytest.mark.parametrize("permit_result", [None, asyncio.TimeoutError()]) async def test_permit_join_with_key(device, permit_result, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) # Consciot bulb ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") @@ -155,10 +155,6 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc ], ) - mocker.patch.object( - app, "permit", new=CoroutineMock(side_effect=[None, permit_result]) - ) - join_disable_install_code = znp_server.reply_once_to( c.AppConfig.BDBSetJoinUsesInstallCodeKey.Req(BdbJoinUsesInstallCodeKey=False), responses=[ @@ -168,6 +164,8 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc await app.startup(auto_form=False) + mocker.patch.object(app, "permit", new=CoroutineMock(side_effect=permit_result)) + with contextlib.nullcontext() if permit_result is None else pytest.raises( asyncio.TimeoutError ): @@ -175,7 +173,7 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc await bdb_add_install_code await join_enable_install_code - assert app.permit.call_count == 2 + assert app.permit.call_count == 1 # The install code policy is reset right after await join_disable_install_code @@ -185,7 +183,7 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc @pytest.mark.parametrize("device", FORMED_ZSTACK3_DEVICES) async def test_permit_join_with_invalid_key(device, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) # Consciot bulb ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") @@ -199,7 +197,7 @@ async def test_permit_join_with_invalid_key(device, make_application): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_device_join(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) mocker.patch.object(app, "handle_join") @@ -219,7 +217,7 @@ async def test_on_zdo_device_join(device, make_application, mocker): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_device_join_and_announce_fast(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) mocker.patch.object(app, "handle_join") @@ -271,12 +269,12 @@ async def test_on_zdo_device_join_and_announce_fast(device, make_application, mo # Everything is cleaned up assert not app._join_announce_tasks - await app.pre_shutdown() + await app.shutdown() @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_device_join_and_announce_slow(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) znp_server.reply_to( @@ -334,12 +332,12 @@ async def test_on_zdo_device_join_and_announce_slow(device, make_application, mo # The announcement will trigger another join indication assert app.handle_join.call_count == 2 - await app.pre_shutdown() + await app.shutdown() @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_unknown_device_discovery(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) mocker.spy(app, "handle_join") @@ -417,14 +415,14 @@ async def test_unknown_device_discovery(device, make_application, mocker): assert new_dev.nwk == new_nwk assert new_dev.ieee == new_ieee - await app.pre_shutdown() + await app.shutdown() @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_unknown_device_discovery_failure(device, make_application, mocker): mocker.patch("zigpy_znp.zigbee.application.IEEE_ADDR_DISCOVERY_TIMEOUT", new=0.1) - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) znp_server.reply_once_to( @@ -438,4 +436,4 @@ async def test_unknown_device_discovery_failure(device, make_application, mocker with pytest.raises(KeyError): await app._get_or_discover_device(nwk=0x3456) - await app.pre_shutdown() + await app.shutdown() diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 960b7647..04739026 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -23,7 +23,7 @@ @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_chosen_dst_endpoint(device, make_application, mocker): - app, znp_server = make_application(device) + app, znp_server = await make_application(device) await app.startup(auto_form=False) build = mocker.patch.object(type(app), "_zstack_build_id", mocker.PropertyMock()) @@ -48,7 +48,7 @@ async def test_chosen_dst_endpoint(device, make_application, mocker): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_zigpy_request(device, make_application): - app, znp_server = make_application(device) + app, znp_server = await make_application(device) await app.startup(auto_form=False) TSN = 6 @@ -108,7 +108,7 @@ async def test_zigpy_request(device, make_application): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_zigpy_request_failure(device, make_application, mocker): - app, znp_server = make_application(device) + app, znp_server = await make_application(device) await app.startup(auto_form=False) TSN = 6 @@ -161,7 +161,7 @@ async def test_zigpy_request_failure(device, make_application, mocker): ], ) async def test_request_addr_mode(device, addr, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -188,7 +188,7 @@ async def test_request_addr_mode(device, addr, make_application, mocker): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_mrequest(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) mocker.patch.object(app, "_send_request", new=CoroutineMock()) group = app.groups.add_group(0x1234, "test group") @@ -206,7 +206,7 @@ async def test_mrequest(device, make_application, mocker): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_mrequest_doesnt_block(device, make_application, event_loop): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) znp_server.reply_once_to( request=c.AF.DataRequestExt.Req( @@ -239,7 +239,7 @@ async def test_mrequest_doesnt_block(device, make_application, event_loop): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_broadcast(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup() znp_server.reply_once_to( @@ -275,7 +275,7 @@ async def test_broadcast(device, make_application, mocker): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_request_concurrency(device, make_application, mocker): - app, znp_server = make_application( + app, znp_server = await make_application( server_cls=device, client_config={"znp_config": {conf.CONF_MAX_CONCURRENT_REQUESTS: 2}}, ) @@ -346,7 +346,7 @@ async def callback(req): async def test_request_concurrency_overflow(device, make_application, mocker): mocker.patch("zigpy_znp.zigbee.application.MAX_WAITING_REQUESTS", new=1) - app, znp_server = make_application( + app, znp_server = await make_application( server_cls=device, client_config={ 'znp_config': {conf.CONF_MAX_CONCURRENT_REQUESTS: 1} } @@ -399,7 +399,7 @@ async def callback(req): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_nonstandard_profile(device, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xFA9E) @@ -456,7 +456,7 @@ async def test_nonstandard_profile(device, make_application): async def test_request_cancellation_shielding( device, make_application, mocker, event_loop ): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -511,7 +511,9 @@ async def inner(): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_request_recovery_route_rediscovery_zdo(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + TSN = 6 + + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -555,7 +557,7 @@ def set_route_discovered(req): request=zdo_request_matcher( dst_addr=t.AddrModeAddress(t.AddrMode.NWK, device.nwk), command_id=zdo_t.ZDOCmd.Active_EP_req, - TSN=6, + TSN=TSN, zdo_NWKAddrOfInterest=device.nwk, ), responses=[ @@ -570,7 +572,7 @@ def set_route_discovered(req): IsBroadcast=t.Bool.false, ClusterId=zdo_t.ZDOCmd.Active_EP_rsp, SecurityUse=0, - TSN=6, + TSN=TSN, MacDst=device.nwk, Data=serialize_zdo_command( command_id=zdo_t.ZDOCmd.Active_EP_rsp, @@ -592,7 +594,7 @@ def set_route_discovered(req): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_request_recovery_route_rediscovery_af(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -661,7 +663,7 @@ def set_route_discovered(req): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_request_recovery_use_ieee_addr(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -729,7 +731,7 @@ def data_confirm_replier(req): async def test_request_recovery_assoc_remove( device_cls, fw_assoc_remove, final_status, make_application, mocker ): - app, znp_server = make_application(server_cls=device_cls) + app, znp_server = await make_application(server_cls=device_cls) await app.startup(auto_form=False) @@ -863,7 +865,7 @@ def assoc_remove(req): async def test_request_recovery_manual_source_route( device, succeed, relays, make_application, mocker ): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -937,7 +939,7 @@ def data_confirm_replier(req): @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_route_discovery_concurrency(device, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -976,7 +978,7 @@ async def test_route_discovery_concurrency(device, make_application): async def test_zdo_from_unknown(device, make_application, caplog, mocker): mocker.patch("zigpy_znp.zigbee.application.IEEE_ADDR_DISCOVERY_TIMEOUT", new=0.1) - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) znp_server.reply_once_to( request=c.ZDO.IEEEAddrReq.Req(partial=True), diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index 45d372e0..2c6147dc 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -1,4 +1,5 @@ import pytest +from zigpy.exceptions import NetworkNotFormed import zigpy_znp.types as t import zigpy_znp.config as conf @@ -8,7 +9,6 @@ from zigpy_znp.types.nvids import ExNvIds, OsalNvIds from ..conftest import ( - ALL_DEVICES, EMPTY_DEVICES, FORMED_DEVICES, CoroutineMock, @@ -62,14 +62,7 @@ async def test_info( make_application, caplog, ): - app, znp_server = make_application(server_cls=device) - - # These should not raise any errors even if our NIB is empty - assert app.pan_id == 0xFFFE # unknown NWK ID - assert app.extended_pan_id == t.EUI64.convert("ff:ff:ff:ff:ff:ff:ff:ff") - assert app.channel is None - assert app.channels is None - assert app.state.network_information.network_key is None + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) @@ -78,25 +71,25 @@ async def test_info( else: assert "Your network is using the insecure" not in caplog.text - assert app.pan_id == pan_id - assert app.extended_pan_id == ext_pan_id - assert app.channel == channel - assert app.channels == channels - assert app.state.network_information.network_key.key == network_key - assert app.state.network_information.network_key.seq == 0 + assert app.state.network_info.pan_id == pan_id + assert app.state.network_info.extended_pan_id == ext_pan_id + assert app.state.network_info.channel == channel + assert app.state.network_info.channel_mask == channels + assert app.state.network_info.network_key.key == network_key + assert app.state.network_info.network_key.seq == 0 - assert app.zigpy_device.manufacturer == "Texas Instruments" - assert app.zigpy_device.model == model + assert app._device.manufacturer == "Texas Instruments" + assert app._device.model == model # Anything to make sure it's set - assert app.zigpy_device.node_desc.maximum_outgoing_transfer_size == 160 + assert app._device.node_desc.maximum_outgoing_transfer_size == 160 await app.shutdown() @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_endpoints(device, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) endpoints = [] znp_server.callback_for_response(c.AF.Register.Req(partial=True), endpoints.append) @@ -105,36 +98,24 @@ async def test_endpoints(device, make_application): # We currently just register two endpoints assert len(endpoints) == 2 - assert 1 in app.zigpy_device.endpoints - assert 2 in app.zigpy_device.endpoints + assert 1 in app._device.endpoints + assert 2 in app._device.endpoints await app.shutdown() @pytest.mark.parametrize("device", EMPTY_DEVICES) async def test_not_configured(device, make_application): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) # We cannot start the application if Z-Stack is not configured and without auto_form - with pytest.raises(RuntimeError): - await app.startup(auto_form=False) - - -@pytest.mark.parametrize("device", ALL_DEVICES) -async def test_bad_nvram_value(device, make_application): - app, znp_server = make_application(server_cls=device) - - # An invalid value is still bad - znp_server._nvram[ExNvIds.LEGACY][OsalNvIds.HAS_CONFIGURED_ZSTACK3] = b"\x00" - znp_server._nvram[ExNvIds.LEGACY][OsalNvIds.HAS_CONFIGURED_ZSTACK1] = b"\x00" - - with pytest.raises(RuntimeError): + with pytest.raises(NetworkNotFormed): await app.startup(auto_form=False) @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_reset(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) # `_reset` should be called at least once to put the radio into a consistent state mocker.spy(ZNP, "reset") @@ -145,25 +126,10 @@ async def test_reset(device, make_application, mocker): await app.shutdown() -@pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_write_nvram(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) - nvram = znp_server._nvram[ExNvIds.LEGACY] - - # Change NVRAM value we should change it back - nvram[OsalNvIds.LOGICAL_TYPE] = t.DeviceLogicalType.EndDevice.serialize() - - assert nvram[OsalNvIds.LOGICAL_TYPE] != t.DeviceLogicalType.Coordinator.serialize() - await app.startup() - assert nvram[OsalNvIds.LOGICAL_TYPE] == t.DeviceLogicalType.Coordinator.serialize() - - await app.shutdown() - - @pytest.mark.parametrize("device", FORMED_DEVICES) @pytest.mark.parametrize("succeed", [True, False]) async def test_tx_power(device, succeed, make_application): - app, znp_server = make_application( + app, znp_server = await make_application( server_cls=device, client_config={conf.CONF_ZNP_CONFIG: {conf.CONF_TX_POWER: 19}}, ) @@ -210,7 +176,7 @@ async def test_tx_power(device, succeed, make_application): @pytest.mark.parametrize("led_mode", ["off", False, "on", True]) @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_led_mode(device, led_mode, make_application): - app, znp_server = make_application( + app, znp_server = await make_application( server_cls=device, client_config={conf.CONF_ZNP_CONFIG: {conf.CONF_LED_MODE: led_mode}}, ) @@ -239,7 +205,7 @@ async def test_led_mode(device, led_mode, make_application): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_auto_form_unnecessary(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) mocker.patch.object(app, "form_network", new=CoroutineMock()) await app.startup(auto_form=True) @@ -251,15 +217,15 @@ async def test_auto_form_unnecessary(device, make_application, mocker): @pytest.mark.parametrize("device", EMPTY_DEVICES) async def test_auto_form_necessary(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) - assert app.channel is None - assert app.channels is None + assert app.state.network_info.channel == 0 + assert app.state.network_info.channel_mask == t.Channels.NO_CHANNELS await app.startup(auto_form=True) - assert app.channel is not None - assert app.channels is not None + assert app.state.network_info.channel != 0 + assert app.state.network_info.channel_mask != t.Channels.NO_CHANNELS nvram = znp_server._nvram[ExNvIds.LEGACY] @@ -276,9 +242,9 @@ async def test_auto_form_necessary(device, make_application, mocker): @pytest.mark.parametrize("device", [FormedZStack1CC2531]) async def test_zstack_build_id_empty(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) - znp_server.reply_once_to( + znp_server.reply_to( c.SYS.Version.Req(), responses=c.SYS.Version.Rsp( TransportRev=2, diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py index fdfd34ba..b6a0b82d 100644 --- a/tests/application/test_zdo_requests.py +++ b/tests/application/test_zdo_requests.py @@ -26,7 +26,7 @@ async def test_mgmt_nwk_update_req( ): mocker.patch("zigpy_znp.zigbee.device.NWK_UPDATE_LOOP_DELAY", 0.1) - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) if change_channel: new_channel = 11 + (26 - znp_server.nib.nwkLogicalChannel) @@ -88,7 +88,7 @@ async def update_channel(req): broadcast_address=zigpy_t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, ) else: - await app.zigpy_device.zdo.Mgmt_NWK_Update_req(update) + await app._device.zdo.Mgmt_NWK_Update_req(update) if change_channel: await nwk_update_req diff --git a/tests/application/test_zigpy_callbacks.py b/tests/application/test_zigpy_callbacks.py index 21d2d250..d90219fb 100644 --- a/tests/application/test_zigpy_callbacks.py +++ b/tests/application/test_zigpy_callbacks.py @@ -28,7 +28,7 @@ def side_effect_(*args, **kwargs): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_relays_message_callback(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) device = mocker.Mock() @@ -47,7 +47,7 @@ async def test_on_zdo_relays_message_callback(device, make_application, mocker): async def test_on_zdo_relays_message_callback_unknown( device, make_application, mocker, caplog ): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) discover_called, discover_mock = awaitable_mock(side_effect=KeyError()) @@ -64,7 +64,7 @@ async def test_on_zdo_relays_message_callback_unknown( @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_device_announce_nwk_change(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) mocker.spy(app, "handle_join") @@ -117,7 +117,7 @@ async def test_on_zdo_device_announce_nwk_change(device, make_application, mocke @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_device_leave_callback(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) mocker.patch.object(app, "handle_leave") @@ -137,14 +137,13 @@ async def test_on_zdo_device_leave_callback(device, make_application, mocker): @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_af_message_callback(device, make_application, mocker): - app, znp_server = make_application(server_cls=device) + app, znp_server = await make_application(server_cls=device) await app.startup(auto_form=False) device = mocker.Mock() discover_called, discover_mock = awaitable_mock(return_value=device) mocker.patch.object(app, "_get_or_discover_device", new=discover_mock) mocker.patch.object(app, "handle_message") - mocker.patch.object(app, "get_device") af_message = c.AF.IncomingMsg.Callback( GroupId=1, @@ -173,7 +172,6 @@ async def test_on_af_message_callback(device, make_application, mocker): device.reset_mock() app.handle_message.reset_mock() - app.get_device.reset_mock() # ZLL message discover_called, discover_mock = awaitable_mock(return_value=device) @@ -189,7 +187,6 @@ async def test_on_af_message_callback(device, make_application, mocker): device.reset_mock() app.handle_message.reset_mock() - app.get_device.reset_mock() # Message on an unknown endpoint (is this possible?) discover_called, discover_mock = awaitable_mock(return_value=device) @@ -205,7 +202,6 @@ async def test_on_af_message_callback(device, make_application, mocker): device.reset_mock() app.handle_message.reset_mock() - app.get_device.reset_mock() # Message from an unknown device discover_called, discover_mock = awaitable_mock(side_effect=KeyError()) diff --git a/tests/conftest.py b/tests/conftest.py index 223cc2ec..76f1be3a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,7 @@ import inspect import logging import pathlib -import contextlib -from unittest.mock import Mock, PropertyMock +from unittest.mock import Mock, PropertyMock, patch import pytest import zigpy.types @@ -12,7 +11,7 @@ try: # Python 3.8 already has this - from unittest.mock import AsyncMock as CoroutineMock # noqa: F401 + from unittest.mock import AsyncMock as CoroutineMock # type: ignore # noqa: F401 except ImportError: from asynctest import CoroutineMock # type:ignore[no-redef] # noqa: F401 @@ -217,20 +216,14 @@ def merge_dicts(a, b): return c -@contextlib.contextmanager -def swap_attribute(obj, name, value): - old_value = getattr(obj, name) - setattr(obj, name, value) - - try: - yield old_value - finally: - setattr(obj, name, old_value) - - @pytest.fixture def make_application(make_znp_server): - def inner(server_cls, client_config=None, server_config=None, **kwargs): + async def inner( + server_cls, + client_config=None, + server_config=None, + **kwargs, + ): default = config_for_port_path(FAKE_SERIAL_PORT) client_config = merge_dicts(default, client_config or {}) @@ -267,9 +260,9 @@ def add_initialized_device(self, *args, **kwargs): app.add_initialized_device = add_initialized_device.__get__(app) - return app, make_znp_server( - server_cls=server_cls, config=server_config, **kwargs - ) + server = make_znp_server(server_cls=server_cls, config=server_config, **kwargs) + + return app, server return inner @@ -358,7 +351,7 @@ def send(self, response): def close(self): # We don't clear listeners on shutdown - with swap_attribute(self, "_listeners", {}): + with patch.object(self, "_listeners", {}): return super().close() @@ -415,6 +408,7 @@ def __init__(self, *args, **kwargs): self.active_endpoints = [] self._nvram = {} + self._orig_nvram = {} self.device_state = t.DeviceState.InitializedNotStarted self.zdo_callbacks = set() @@ -821,6 +815,15 @@ def reset_req(self, request, *, _handle_startup_reset=True): OsalNvIds.STARTUP_OPTION ] = t.StartupOptions.NONE.serialize() + # Resetting recreates the EXTADDR NVRAM item + if ( + OsalNvIds.EXTADDR not in self._nvram.get(ExNvIds.LEGACY, {}) + and self._orig_nvram + ): + self._nvram[ExNvIds.LEGACY][OsalNvIds.EXTADDR] = self._orig_nvram[ + ExNvIds.LEGACY + ][OsalNvIds.EXTADDR] + version = self.version_replier(None) return c.SYS.ResetInd.Callback( @@ -1303,43 +1306,43 @@ def led_responder(self, req): class FormedLaunchpadCC26X2R1(BaseLaunchpadCC26X2R1): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._nvram = load_nvram_json("CC2652R-ZStack4.formed.json") + self._orig_nvram = simple_deepcopy(self._nvram) class ResetLaunchpadCC26X2R1(BaseLaunchpadCC26X2R1): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._nvram = load_nvram_json("CC2652R-ZStack4.reset.json") + self._orig_nvram = simple_deepcopy(self._nvram) class FormedZStack3CC2531(BaseZStack3CC2531): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._nvram = load_nvram_json("CC2531-ZStack3.formed.json") + self._orig_nvram = simple_deepcopy(self._nvram) class ResetZStack3CC2531(BaseZStack3CC2531): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._nvram = load_nvram_json("CC2531-ZStack3.reset.json") + self._orig_nvram = simple_deepcopy(self._nvram) class FormedZStack1CC2531(BaseZStack1CC2531): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._nvram = load_nvram_json("CC2531-ZStack1.formed.json") + self._orig_nvram = simple_deepcopy(self._nvram) class ResetZStack1CC2531(BaseZStack1CC2531): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._nvram = load_nvram_json("CC2531-ZStack1.reset.json") + self._orig_nvram = simple_deepcopy(self._nvram) EMPTY_DEVICES = [ diff --git a/tests/tools/test_form_network.py b/tests/tools/test_form_network.py index 92b587de..e69de29b 100644 --- a/tests/tools/test_form_network.py +++ b/tests/tools/test_form_network.py @@ -1,23 +0,0 @@ -import pytest - -from zigpy_znp.types.nvids import ExNvIds, OsalNvIds -from zigpy_znp.tools.form_network import main as form_network - -from ..conftest import ALL_DEVICES, EMPTY_DEVICES - - -@pytest.mark.parametrize("device", ALL_DEVICES) -async def test_form_network(device, make_znp_server): - znp_server = make_znp_server(server_cls=device) - legacy = znp_server._nvram[ExNvIds.LEGACY] - - if device in EMPTY_DEVICES: - assert OsalNvIds.HAS_CONFIGURED_ZSTACK1 not in legacy - assert OsalNvIds.HAS_CONFIGURED_ZSTACK3 not in legacy - - await form_network([znp_server._port_path]) - - assert ( - OsalNvIds.HAS_CONFIGURED_ZSTACK1 in legacy - or OsalNvIds.HAS_CONFIGURED_ZSTACK3 in legacy - ) diff --git a/tests/tools/test_network_backup_restore.py b/tests/tools/test_network_backup_restore.py index 7b681daa..f3b16d34 100644 --- a/tests/tools/test_network_backup_restore.py +++ b/tests/tools/test_network_backup_restore.py @@ -4,6 +4,7 @@ import pytest import zigpy.state from jsonschema import ValidationError +from zigpy.exceptions import NetworkNotFormed import zigpy_znp.types as t from zigpy_znp.znp import security @@ -24,7 +25,7 @@ ) from ..application.test_startup import DEV_NETWORK_SETTINGS -BARE_NETWORK_INFO = zigpy.state.NetworkInformation( +BARE_NETWORK_INFO = zigpy.state.NetworkInfo( extended_pan_id=t.EUI64.convert("ab:de:fa:bc:de:fa:bc:de"), channel=None, channel_mask=None, @@ -138,7 +139,7 @@ def test_schema_validation_device_key_info(backup_json): async def test_network_backup_empty(device, make_znp_server): znp_server = make_znp_server(server_cls=device) - with pytest.raises(RuntimeError): + with pytest.raises(NetworkNotFormed): await network_backup([znp_server._port_path, "-o", "-"]) @@ -249,19 +250,19 @@ async def test_network_restore_pick_optimal_tclk( assert backup_json2["stack_specific"]["zstack"]["tclk_seed"] == old_tclk_seed -async def test_tc_frame_counter_zstack1(make_connected_znp): +async def test_nwk_frame_counter_zstack1(make_connected_znp): znp, znp_server = await make_connected_znp(BaseZStack1CC2531) znp_server._nvram[ExNvIds.LEGACY] = { OsalNvIds.NWKKEY: b"\x01" + b"\xAB" * 16 + b"\x78\x56\x34\x12" } - assert (await security.read_tc_frame_counter(znp)) == 0x12345678 + assert (await security.read_nwk_frame_counter(znp)) == 0x12345678 - await security.write_tc_frame_counter(znp, 0xAABBCCDD) - assert (await security.read_tc_frame_counter(znp)) == 0xAABBCCDD + await security.write_nwk_frame_counter(znp, 0xAABBCCDD) + assert (await security.read_nwk_frame_counter(znp)) == 0xAABBCCDD -async def test_tc_frame_counter_zstack30(make_connected_znp): +async def test_nwk_frame_counter_zstack30(make_connected_znp): znp, znp_server = await make_connected_znp(BaseZStack3CC2531) znp.network_info = BARE_NETWORK_INFO znp.node_info = BARE_NODE_INFO @@ -280,19 +281,19 @@ async def test_tc_frame_counter_zstack30(make_connected_znp): + b"\xFF" * 8, } - assert (await security.read_tc_frame_counter(znp)) == 0x00000001 + assert (await security.read_nwk_frame_counter(znp)) == 0x00000001 # If we change the EPID, the generic entry will be used znp.network_info = dataclasses.replace( znp.network_info, extended_pan_id=t.EUI64.convert("11:22:33:44:55:66:77:88") ) - assert (await security.read_tc_frame_counter(znp)) == 0x00000002 + assert (await security.read_nwk_frame_counter(znp)) == 0x00000002 - await security.write_tc_frame_counter(znp, 0xAABBCCDD) - assert (await security.read_tc_frame_counter(znp)) == 0xAABBCCDD + await security.write_nwk_frame_counter(znp, 0xAABBCCDD) + assert (await security.read_nwk_frame_counter(znp)) == 0xAABBCCDD -async def test_tc_frame_counter_zstack33(make_connected_znp): +async def test_nwk_frame_counter_zstack33(make_connected_znp): znp, znp_server = await make_connected_znp(BaseLaunchpadCC26X2R1) znp.network_info = BARE_NETWORK_INFO znp.node_info = BARE_NODE_INFO @@ -312,7 +313,7 @@ async def test_tc_frame_counter_zstack33(make_connected_znp): }, } - assert (await security.read_tc_frame_counter(znp)) == 0x00000002 + assert (await security.read_nwk_frame_counter(znp)) == 0x00000002 # If we change the EPID, the generic entry will be used. It doesn't exist. znp.network_info = dataclasses.replace( @@ -320,7 +321,7 @@ async def test_tc_frame_counter_zstack33(make_connected_znp): ) with pytest.raises(ValueError): - await security.read_tc_frame_counter(znp) + await security.read_nwk_frame_counter(znp) # Writes similarly will fail old_nvram_state = repr(znp_server._nvram) @@ -328,8 +329,8 @@ async def test_tc_frame_counter_zstack33(make_connected_znp): # And the NVRAM will be untouched assert repr(znp_server._nvram) == old_nvram_state - await security.write_tc_frame_counter(znp, 0x98765432) - assert (await security.read_tc_frame_counter(znp)) == 0x98765432 + await security.write_nwk_frame_counter(znp, 0x98765432) + assert (await security.read_nwk_frame_counter(znp)) == 0x98765432 def ieee_and_key(text) -> zigpy.state.Key: diff --git a/tests/tools/test_nvram.py b/tests/tools/test_nvram.py index 9b3d39d5..251245b8 100644 --- a/tests/tools/test_nvram.py +++ b/tests/tools/test_nvram.py @@ -136,7 +136,6 @@ async def test_nvram_reset(device, make_znp_server): await nvram_reset([znp_server._port_path]) # Nothing exists but the synthetic POLL_RATE_OLD16 - assert len(znp_server._nvram[ExNvIds.LEGACY].keys()) == 1 assert len([v for v in znp_server._nvram.values() if v]) == 1 assert OsalNvIds.POLL_RATE_OLD16 in znp_server._nvram[ExNvIds.LEGACY] @@ -153,7 +152,6 @@ async def test_nvram_reset_clear(device, make_znp_server, caplog): assert "will be removed in a future release" in caplog.text # Nothing exists but the synthetic POLL_RATE_OLD16 - assert len(znp_server._nvram[ExNvIds.LEGACY].keys()) == 1 assert len([v for v in znp_server._nvram.values() if v]) == 1 assert OsalNvIds.POLL_RATE_OLD16 in znp_server._nvram[ExNvIds.LEGACY] diff --git a/zigpy_znp/api.py b/zigpy_znp/api.py index ecc71e60..bf0d0df5 100644 --- a/zigpy_znp/api.py +++ b/zigpy_znp/api.py @@ -13,7 +13,9 @@ import zigpy.state import async_timeout import zigpy.zdo.types as zdo_t +from zigpy.exceptions import NetworkNotFormed +import zigpy_znp import zigpy_znp.const as const import zigpy_znp.types as t import zigpy_znp.config as conf @@ -38,12 +40,15 @@ # All of these are in seconds +STARTUP_TIMEOUT = 15 AFTER_BOOTLOADER_SKIP_BYTE_DELAY = 2.5 NETWORK_COMMISSIONING_TIMEOUT = 30 BOOTLOADER_PIN_TOGGLE_DELAY = 0.15 CONNECT_PING_TIMEOUT = 0.50 CONNECT_PROBE_TIMEOUT = 10 +NVRAM_MIGRATION_ID = 1 + class ZNP: def __init__(self, config: conf.ConfigType): @@ -58,7 +63,7 @@ def __init__(self, config: conf.ConfigType): self.version = None # type: float self.nvram = NVRAMHelper(self) - self.network_info: zigpy.state.NetworkInformation = None + self.network_info: zigpy.state.NetworkInfo = None self.node_info: zigpy.state.NodeInfo = None def set_application(self, app): @@ -115,17 +120,31 @@ async def load_network_info(self, *, load_devices=False): ) if not is_on_network: - raise ValueError("Device is not a part of a network") + raise NetworkNotFormed("Device is not a part of a network") + + ieee = await self.nvram.osal_read(OsalNvIds.EXTADDR, item_type=t.EUI64) + logical_type = await self.nvram.osal_read( + OsalNvIds.LOGICAL_TYPE, item_type=t.DeviceLogicalType + ) + + node_info = zigpy.state.NodeInfo( + ieee=ieee, + nwk=nib.nwkDevAddress, + logical_type=zdo_t.LogicalType(logical_type), + ) key_desc = await self.nvram.osal_read( OsalNvIds.NWK_ACTIVE_KEY_INFO, item_type=t.NwkKeyDesc ) - tc_frame_counter = await security.read_tc_frame_counter( + nwk_frame_counter = await security.read_nwk_frame_counter( self, ext_pan_id=nib.extendedPANID ) - network_info = zigpy.state.NetworkInformation( + version = await self.request(c.SYS.Version.Req()) + + network_info = zigpy.state.NetworkInfo( + source=f"zigpy-znp@{zigpy_znp.__version__}", extended_pan_id=nib.extendedPANID, pan_id=nib.nwkPanId, nwk_update_id=nib.nwkUpdateId, @@ -135,15 +154,22 @@ async def load_network_info(self, *, load_devices=False): network_key=zigpy.state.Key( key=key_desc.Key, seq=key_desc.KeySeqNum, - tx_counter=tc_frame_counter, - rx_counter=None, - partner_ieee=None, + tx_counter=nwk_frame_counter, + rx_counter=0, + partner_ieee=node_info.ieee, + ), + tc_link_key=zigpy.state.Key( + key=const.DEFAULT_TC_LINK_KEY, + seq=0, + tx_counter=0, + rx_counter=0, + partner_ieee=node_info.ieee, ), - tc_link_key=None, children=[], nwk_addresses={}, key_table=[], - stack_specific=None, + stack_specific={}, + metadata={"zstack": version.as_dict()}, ) tclk_seed = None @@ -187,87 +213,23 @@ async def load_network_info(self, *, load_devices=False): ) if dev.is_child: - network_info.children.append(dev.node_info) - elif dev.node_info.nwk is not None: + network_info.children.append(dev.node_info.ieee) + + if dev.node_info.nwk is not None: network_info.nwk_addresses[dev.node_info.ieee] = dev.node_info.nwk if dev.key is not None: network_info.key_table.append(dev.key) - ieee = await self.nvram.osal_read(OsalNvIds.EXTADDR, item_type=t.EUI64) - logical_type = await self.nvram.osal_read( - OsalNvIds.LOGICAL_TYPE, item_type=t.DeviceLogicalType - ) - - node_info = zigpy.state.NodeInfo( - ieee=ieee, - nwk=nib.nwkDevAddress, - logical_type=zdo_t.LogicalType(logical_type), - ) - self.network_info = network_info self.node_info = node_info - async def write_network_info( - self, - *, - network_info: zigpy.state.NetworkInformation, - node_info: zigpy.state.NodeInfo, - ) -> None: - """ - Writes network and node state to NVRAM. - """ - - from zigpy_znp.znp import security - - # Delete any existing NV items that store formation state - await self.nvram.osal_delete(OsalNvIds.HAS_CONFIGURED_ZSTACK1) - await self.nvram.osal_delete(OsalNvIds.HAS_CONFIGURED_ZSTACK3) - await self.nvram.osal_delete(OsalNvIds.ZIGPY_ZNP_MIGRATION_ID) - await self.nvram.osal_delete(OsalNvIds.BDBNODEISONANETWORK) - - # Instruct Z-Stack to reset everything on the next boot - await self.nvram.osal_write( - OsalNvIds.STARTUP_OPTION, - t.StartupOptions.ClearState | t.StartupOptions.ClearConfig, - ) - - await self.reset() - - # Form a network with completely random settings to get NVRAM to a known state - for item, value in { - OsalNvIds.PANID: t.uint16_t(0xFFFF), - OsalNvIds.APS_USE_EXT_PANID: t.EUI64(os.urandom(8)), - OsalNvIds.PRECFGKEY: os.urandom(16), - # XXX: Z2M requires this item to be False - OsalNvIds.PRECFGKEYS_ENABLE: t.Bool(False), - # Z-Stack will scan all of thse channels during formation - OsalNvIds.CHANLIST: const.STARTUP_CHANNELS, - }.items(): - await self.nvram.osal_write(item, value, create=True) - - # Z-Stack 3+ ignores `CHANLIST` - if self.version > 1.2: - await self.request( - c.AppConfig.BDBSetChannel.Req( - IsPrimary=True, Channel=const.STARTUP_CHANNELS - ), - RspStatus=t.Status.SUCCESS, - ) - await self.request( - c.AppConfig.BDBSetChannel.Req( - IsPrimary=False, Channel=t.Channels.NO_CHANNELS - ), - RspStatus=t.Status.SUCCESS, - ) - + async def start_network(self): # Both startup sequences end with the same callback started_as_coordinator = self.wait_for_response( c.ZDO.StateChangeInd.Callback(State=t.DeviceState.StartedAsCoordinator) ) - LOGGER.debug("Forming temporary network") - # Handle the startup progress messages async with self.capture_responses( [ @@ -295,22 +257,31 @@ async def write_network_info( timeout=NETWORK_COMMISSIONING_TIMEOUT, ) - if ( - commissioning_rsp.Status - != c.app_config.BDBCommissioningStatus.Success + # This is the correct startup sequence according to the forums, + # including the formation failure error. Success is only returned + # when the network is first formed. + if commissioning_rsp.Status not in ( + c.app_config.BDBCommissioningStatus.FormationFailure, + c.app_config.BDBCommissioningStatus.Success, ): raise RuntimeError( f"Network formation failed: {commissioning_rsp}" ) else: # In Z-Stack 1.2.2, StartupFromApp actually does what it says - await self.request( - c.ZDO.StartupFromApp.Req(StartDelay=100), - RspState=c.zdo.StartupState.NewNetworkState, - ) + rsp = await self.request(c.ZDO.StartupFromApp.Req(StartDelay=100)) + + if rsp.State not in ( + c.zdo.StartupState.NewNetworkState, + c.zdo.StartupState.RestoredNetworkState, + ): + raise InvalidCommandResponse( + f"Invalid startup response state: {rsp.State}", rsp + ) # Both versions still end with this callback - await started_as_coordinator + async with async_timeout.timeout(STARTUP_TIMEOUT): + await started_as_coordinator except asyncio.TimeoutError as e: raise RuntimeError( "Network formation refused, RF environment is likely too noisy." @@ -337,6 +308,61 @@ async def write_network_info( await asyncio.sleep(1) + async def write_network_info( + self, + *, + network_info: zigpy.state.NetworkInfo, + node_info: zigpy.state.NodeInfo, + ) -> None: + """ + Writes network and node state to NVRAM. + """ + + from zigpy_znp.znp import security + + # Delete any existing NV items that store formation state + await self.nvram.osal_delete(OsalNvIds.HAS_CONFIGURED_ZSTACK1) + await self.nvram.osal_delete(OsalNvIds.HAS_CONFIGURED_ZSTACK3) + await self.nvram.osal_delete(OsalNvIds.ZIGPY_ZNP_MIGRATION_ID) + await self.nvram.osal_delete(OsalNvIds.BDBNODEISONANETWORK) + + # Instruct Z-Stack to reset everything on the next boot + await self.nvram.osal_write( + OsalNvIds.STARTUP_OPTION, + t.StartupOptions.ClearState | t.StartupOptions.ClearConfig, + ) + + await self.reset() + + # Form a network with completely random settings to get NVRAM to a known state + for item, value in { + OsalNvIds.PANID: t.uint16_t(0xFFFF), + OsalNvIds.APS_USE_EXT_PANID: t.EUI64(os.urandom(8)), + OsalNvIds.PRECFGKEY: os.urandom(16), + # XXX: Z2M requires this item to be False + OsalNvIds.PRECFGKEYS_ENABLE: t.Bool(False), + # Z-Stack will scan all of thse channels during formation + OsalNvIds.CHANLIST: const.STARTUP_CHANNELS, + }.items(): + await self.nvram.osal_write(item, value, create=True) + + # Z-Stack 3+ ignores `CHANLIST` + if self.version > 1.2: + await self.request( + c.AppConfig.BDBSetChannel.Req( + IsPrimary=True, Channel=const.STARTUP_CHANNELS + ), + RspStatus=t.Status.SUCCESS, + ) + await self.request( + c.AppConfig.BDBSetChannel.Req( + IsPrimary=False, Channel=t.Channels.NO_CHANNELS + ), + RspStatus=t.Status.SUCCESS, + ) + + LOGGER.debug("Forming temporary network") + await self.start_network() await self.reset() LOGGER.debug("Writing actual network settings") @@ -369,33 +395,52 @@ async def write_network_info( OsalNvIds.EXTENDED_PAN_ID: network_info.extended_pan_id, OsalNvIds.PRECFGKEY: key_info.Active.Key, OsalNvIds.CHANLIST: network_info.channel_mask, - OsalNvIds.EXTADDR: node_info.ieee, + # If the EXTADDR entry is deleted, Z-Stack resets it to the hardware address + OsalNvIds.EXTADDR: ( + None if node_info.ieee == t.EUI64.UNKNOWN else node_info.ieee + ), OsalNvIds.LOGICAL_TYPE: t.DeviceLogicalType(node_info.logical_type), OsalNvIds.NWK_ACTIVE_KEY_INFO: key_info.Active, - OsalNvIds.NWK_ALTERN_KEY_INFO: const.EMPTY_KEY, + OsalNvIds.NWK_ALTERN_KEY_INFO: key_info.Active, } tclk_seed = None - if self.version > 1.2: + if self.version == 1.2: + # TCLK_SEED is TCLK_TABLE_START in Z-Stack 1 + nvram[OsalNvIds.TCLK_SEED] = t.TCLinkKey( + ExtAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"), # global + Key=network_info.tc_link_key.key, + TxFrameCounter=0, + RxFrameCounter=0, + ) + else: + if network_info.tc_link_key.key != const.DEFAULT_TC_LINK_KEY: + LOGGER.warning( + "TC link key is configured at build time in Z-Stack 3 and cannot be" + " changed at runtime: %s", + network_info.tc_link_key.key, + ) + if ( network_info.stack_specific is not None and network_info.stack_specific.get("zstack", {}).get("tclk_seed") ): - tclk_seed, _ = t.KeyData.deserialize( + tclk_seed = t.KeyData( bytes.fromhex(network_info.stack_specific["zstack"]["tclk_seed"]) ) else: tclk_seed = t.KeyData(os.urandom(16)) nvram[OsalNvIds.TCLK_SEED] = tclk_seed - else: - nvram[OsalNvIds.TCLK_SEED] = const.DEFAULT_TC_LINK_KEY for key, value in nvram.items(): - await self.nvram.osal_write(key, value, create=True) + if value is None: + await self.nvram.osal_delete(key) + else: + await self.nvram.osal_write(key, value, create=True) - await security.write_tc_frame_counter( + await security.write_nwk_frame_counter( self, network_info.network_key.tx_counter, ext_pan_id=network_info.extended_pan_id, @@ -403,10 +448,12 @@ async def write_network_info( devices = {} - for child in network_info.children or []: - devices[child.ieee] = security.StoredDevice( - node_info=dataclasses.replace( - child, nwk=0xFFFE if child.nwk is None else child.nwk + for ieee in network_info.children or []: + devices[ieee] = security.StoredDevice( + node_info=zigpy.state.NodeInfo( + nwk=network_info.nwk_addresses.get(ieee, 0xFFFE), + ieee=ieee, + logical_type=zdo_t.LogicalType.EndDevice, ), key=None, is_child=True, @@ -453,6 +500,11 @@ async def write_network_info( counter_increment=0, ) + # Prevent an unnecessary NVRAM migration from running + await self.nvram.osal_write( + OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, t.uint8_t(NVRAM_MIGRATION_ID), create=True + ) + if self.version == 1.2: await self.nvram.osal_write( OsalNvIds.HAS_CONFIGURED_ZSTACK1, @@ -466,18 +518,84 @@ async def write_network_info( create=True, ) + # Reset after writing network settings to allow Z-Stack to recreate NVRAM items + # that were intentionally deleted. + await self.reset() + LOGGER.debug("Done!") - async def reset(self) -> None: + async def migrate_nvram(self) -> bool: + """ + Migrates NVRAM entries using the `ZIGPY_ZNP_MIGRATION_ID` NVRAM item. + Returns `True` if a migration was performed, `False` otherwise. + """ + + from zigpy_znp.znp import security + + try: + migration_id = await self.nvram.osal_read( + OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, item_type=t.uint8_t + ) + except KeyError: + migration_id = 0 + + initial_migration_id = migration_id + + # Migration 1: empty `ADDRMGR` entries are version-dependent and were improperly + # written for CC253x devices. + # + # This migration is stateless and can safely be run more than once: + # the only downside is that startup times increase by 10s on newer + # coordinators, which is why the migration ID is persisted. + if migration_id < 1: + try: + entries = await security.read_addr_manager_entries(self) + except KeyError: + pass + else: + fixed_entries = [] + + for entry in entries: + if entry.extAddr != t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"): + fixed_entries.append(entry) + elif self.version == 3.30: + fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3) + else: + fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1) + + if entries != fixed_entries: + LOGGER.warning( + "Repairing %d invalid empty address manager entries (total %d)", + sum(i != j for i, j in zip(entries, fixed_entries)), + len(entries), + ) + await security.write_addr_manager_entries(self, fixed_entries) + + migration_id = 1 + + if initial_migration_id == migration_id: + return False + + await self.nvram.osal_write( + OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, t.uint8_t(migration_id), create=True + ) + await self.reset() + + return True + + async def reset(self, *, wait_for_reset: bool = True) -> None: """ Performs a soft reset within Z-Stack. A hard reset resets the serial port, causing the device to disconnect. """ - await self.request_callback_rsp( - request=c.SYS.ResetReq.Req(Type=t.ResetType.Soft), - callback=c.SYS.ResetInd.Callback(partial=True), - ) + if wait_for_reset: + await self.request_callback_rsp( + request=c.SYS.ResetReq.Req(Type=t.ResetType.Soft), + callback=c.SYS.ResetInd.Callback(partial=True), + ) + else: + await self.request(c.SYS.ResetReq.Req(Type=t.ResetType.Soft)) @property def _port_path(self) -> str: @@ -672,7 +790,7 @@ def frame_received(self, frame: GeneralFrame) -> bool | None: if frame.header not in c.COMMANDS_BY_ID: LOGGER.error("Received an unknown frame: %s", frame) - return None + return False command_cls = c.COMMANDS_BY_ID[frame.header] @@ -683,7 +801,7 @@ def frame_received(self, frame: GeneralFrame) -> bool | None: # https://github.com/home-assistant/core/issues/50005 if command_cls == c.ZDO.ParentAnnceRsp.Callback: LOGGER.warning("Failed to parse broken %s as %s", frame, command_cls) - return None + return False raise diff --git a/zigpy_znp/const.py b/zigpy_znp/const.py index 797d340e..d1dd0a00 100644 --- a/zigpy_znp/const.py +++ b/zigpy_znp/const.py @@ -4,12 +4,7 @@ Z2M_EXT_PAN_ID = t.EUI64.convert("DD:DD:DD:DD:DD:DD:DD:DD") Z2M_NETWORK_KEY = t.KeyData([1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13]) -DEFAULT_TC_LINK_KEY = t.TCLinkKey( - ExtAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"), # global - Key=t.KeyData(b"ZigBeeAlliance09"), - TxFrameCounter=0, - RxFrameCounter=0, -) +DEFAULT_TC_LINK_KEY = t.KeyData(b"ZigBeeAlliance09") ZSTACK_CONFIGURE_SUCCESS = t.uint8_t(0x55) EMPTY_ADDR_MGR_ENTRY_ZSTACK1 = t.AddrMgrEntry( diff --git a/zigpy_znp/tools/energy_scan.py b/zigpy_znp/tools/energy_scan.py index e05f0c13..3eb0fb85 100644 --- a/zigpy_znp/tools/energy_scan.py +++ b/zigpy_znp/tools/energy_scan.py @@ -5,6 +5,7 @@ from collections import deque, defaultdict import zigpy.zdo.types as zdo_t +from zigpy.exceptions import NetworkNotFormed import zigpy_znp.types as t from zigpy_znp.tools.common import setup_parser @@ -18,10 +19,11 @@ async def perform_energy_scan(radio_path, num_scans=None): config = ControllerApplication.SCHEMA({"device": {"path": radio_path}}) app = ControllerApplication(config) + await app.connect() try: - await app.startup(read_only=True) - except RuntimeError as e: + await app.start_network(read_only=True) + except NetworkNotFormed as e: LOGGER.error("Could not start application: %s", e) LOGGER.error("Form a network with `python -m zigpy_znp.tools.form_network`") return diff --git a/zigpy_znp/tools/form_network.py b/zigpy_znp/tools/form_network.py deleted file mode 100644 index e6f693b5..00000000 --- a/zigpy_znp/tools/form_network.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -import asyncio -import logging - -from zigpy_znp.tools.common import setup_parser -from zigpy_znp.zigbee.application import ControllerApplication - -LOGGER = logging.getLogger(__name__) - - -async def form_network(radio_path): - LOGGER.info("Starting up zigpy-znp") - - config = ControllerApplication.SCHEMA({"device": {"path": radio_path}}) - app = ControllerApplication(config) - - await app.startup(force_form=True) - await app.shutdown() - - -async def main(argv): - parser = setup_parser("Form a network with randomly generated settings") - args = parser.parse_args(argv) - - await form_network(args.serial) - - -if __name__ == "__main__": - asyncio.run(main(sys.argv[1:])) # pragma: no cover diff --git a/zigpy_znp/tools/network_backup.py b/zigpy_znp/tools/network_backup.py index 5495af87..f5f9c521 100644 --- a/zigpy_znp/tools/network_backup.py +++ b/zigpy_znp/tools/network_backup.py @@ -18,7 +18,7 @@ def zigpy_state_to_json_backup( - network_info: zigpy.state.NetworkInformation, node_info: zigpy.state.NodeInfo + network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo ) -> t.JSONType: devices = {} @@ -29,12 +29,11 @@ def zigpy_state_to_json_backup( "is_child": False, } - for child in network_info.children: - devices[child.ieee] = { - "ieee_address": child.ieee.serialize()[::-1].hex(), - "nwk_address": child.nwk.serialize()[::-1].hex() - if child.nwk is not None - else None, + for ieee in network_info.children: + nwk = network_info.nwk_addresses.get(ieee, None) + devices[ieee] = { + "ieee_address": ieee.serialize()[::-1].hex(), + "nwk_address": nwk.serialize()[::-1].hex() if nwk is not None else None, "is_child": True, } @@ -76,10 +75,7 @@ def zigpy_state_to_json_backup( async def backup_network(znp: ZNP) -> t.JSONType: - try: - await znp.load_network_info(load_devices=True) - except ValueError as e: - raise RuntimeError("Failed to load network info") from e + await znp.load_network_info(load_devices=True) obj = zigpy_state_to_json_backup( network_info=znp.network_info, diff --git a/zigpy_znp/tools/network_restore.py b/zigpy_znp/tools/network_restore.py index 2e83a729..e3c9eafb 100644 --- a/zigpy_znp/tools/network_restore.py +++ b/zigpy_znp/tools/network_restore.py @@ -7,14 +7,16 @@ import zigpy.state import zigpy.zdo.types as zdo_t +import zigpy_znp.const as const import zigpy_znp.types as t +from zigpy_znp.api import ZNP from zigpy_znp.tools.common import ClosableFileType, setup_parser, validate_backup_json from zigpy_znp.zigbee.application import ControllerApplication def json_backup_to_zigpy_state( backup: t.JSONType, -) -> tuple[zigpy.state.NetworkInformation, zigpy.state.NodeInfo]: +) -> tuple[zigpy.state.NetworkInfo, zigpy.state.NodeInfo]: """ Converts a JSON backup into a zigpy network and node info tuple. """ @@ -26,7 +28,7 @@ def json_backup_to_zigpy_state( bytes.fromhex(backup["coordinator_ieee"])[::-1] ) - network_info = zigpy.state.NetworkInformation() + network_info = zigpy.state.NetworkInfo() network_info.pan_id, _ = t.NWK.deserialize(bytes.fromhex(backup["pan_id"])[::-1]) network_info.extended_pan_id, _ = t.EUI64.deserialize( bytes.fromhex(backup["extended_pan_id"])[::-1] @@ -37,7 +39,8 @@ def json_backup_to_zigpy_state( network_info.channel_mask = t.Channels.from_channel_list(backup["channel_mask"]) network_info.security_level = backup["security_level"] network_info.stack_specific = backup.get("stack_specific") - network_info.tc_link_key = None + network_info.tc_link_key = zigpy.state.Key() + network_info.tc_link_key.key = const.DEFAULT_TC_LINK_KEY network_info.network_key = zigpy.state.Key() network_info.network_key.key, _ = t.KeyData.deserialize( @@ -64,8 +67,9 @@ def json_backup_to_zigpy_state( # The `is_child` key is currently optional if obj.get("is_child", True): - network_info.children.append(node) - elif node.nwk is not None: + network_info.children.append(node.ieee) + + if node.nwk is not None: network_info.nwk_addresses[node.ieee] = node.nwk if "link_key" in obj: @@ -92,13 +96,11 @@ async def restore_network( network_info, node_info = json_backup_to_zigpy_state(backup) network_info.network_key.tx_counter += counter_increment - config = ControllerApplication.SCHEMA({"device": {"path": radio_path}}) - app = ControllerApplication(config) - - await app.startup(force_form=True) - await app.write_network_info(network_info=network_info, node_info=node_info) - - await app.pre_shutdown() + znp = ZNP(ControllerApplication.SCHEMA({"device": {"path": radio_path}})) + await znp.connect() + await znp.write_network_info(network_info=network_info, node_info=node_info) + await znp.reset() + znp.close() async def main(argv: list[str]) -> None: diff --git a/zigpy_znp/types/commands.py b/zigpy_znp/types/commands.py index 47c542ed..f8e91a0a 100644 --- a/zigpy_znp/types/commands.py +++ b/zigpy_znp/types/commands.py @@ -1,6 +1,7 @@ from __future__ import annotations import enum +import typing import logging import dataclasses @@ -465,6 +466,13 @@ def replace(self, **kwargs) -> CommandBase: return type(self)(partial=self._partial, **params) + def as_dict(self) -> dict[str, typing.Any]: + """ + Converts the command into a dictionary. + """ + + return {p.name: v for p, v in self._bound_params.values()} + def __eq__(self, other): return type(self) is type(other) and self._bound_params == other._bound_params diff --git a/zigpy_znp/types/named.py b/zigpy_znp/types/named.py index f492b109..d622b234 100644 --- a/zigpy_znp/types/named.py +++ b/zigpy_znp/types/named.py @@ -23,7 +23,7 @@ LOGGER = logging.getLogger(__name__) -JSONType = typing.Union[typing.Dict[str, typing.Any], typing.List[typing.Any]] +JSONType = typing.Dict[str, typing.Any] class AddrMode(basic.enum_uint8): diff --git a/zigpy_znp/zigbee/application.py b/zigpy_znp/zigbee/application.py index de13a8bb..354edb70 100644 --- a/zigpy_znp/zigbee/application.py +++ b/zigpy_znp/zigbee/application.py @@ -2,7 +2,6 @@ import os import time -import random import asyncio import logging import itertools @@ -16,12 +15,10 @@ import zigpy.config import zigpy.device import async_timeout -import zigpy.endpoint import zigpy.profiles import zigpy.zdo.types as zdo_t import zigpy.application -from zigpy.zcl import clusters -from zigpy.types import ExtendedPanId, deserialize as list_deserialize +from zigpy.types import deserialize as list_deserialize from zigpy.exceptions import DeliveryError import zigpy_znp.const as const @@ -29,7 +26,6 @@ import zigpy_znp.config as conf import zigpy_znp.commands as c from zigpy_znp.api import ZNP -from zigpy_znp.znp import security from zigpy_znp.utils import combine_concurrent_calls from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse from zigpy_znp.types.nvids import OsalNvIds @@ -37,11 +33,10 @@ ZDO_ENDPOINT = 0 ZHA_ENDPOINT = 1 -ZLL_ENDPOINT = 2 - ZDO_PROFILE = 0x0000 # All of these are in seconds +PROBE_TIMEOUT = 5 STARTUP_TIMEOUT = 5 DATA_CONFIRM_TIMEOUT = 8 IEEE_ADDR_DISCOVERY_TIMEOUT = 5 @@ -85,51 +80,54 @@ def __init__(self, config: conf.ConfigType): self._znp: ZNP | None = None # It's simpler to work with Task objects if they're never actually None - self._reconnect_task = asyncio.Future() + self._reconnect_task: asyncio.Future = asyncio.Future() self._reconnect_task.cancel() - self._watchdog_task = asyncio.Future() + self._watchdog_task: asyncio.Future = asyncio.Future() self._watchdog_task.cancel() self._version_rsp = None self._concurrent_requests_semaphore = None self._currently_waiting_requests = 0 - self._join_announce_tasks = {} + self._join_announce_tasks: dict[t.EUI64, asyncio.TimerHandle] = {} ################################################################## # Implementation of the core zigpy ControllerApplication methods # ################################################################## @classmethod - async def probe(cls, device_config: conf.ConfigType) -> bool: - """ - Checks whether the device represented by `device_config` is a valid ZNP radio. - Doesn't throw any errors. - """ - - znp = ZNP(conf.CONFIG_SCHEMA({conf.CONF_DEVICE: device_config})) - LOGGER.debug("Probing %s", znp._port_path) - + async def probe(cls, device_config): try: - # `ZNP.connect` times out on its own - await znp.connect() - except Exception as e: - LOGGER.debug( - "Failed to probe ZNP radio with config %s", device_config, exc_info=e - ) + async with async_timeout.timeout(PROBE_TIMEOUT): + return await super().probe(device_config) + except asyncio.TimeoutError: return False - else: - return True - finally: - znp.close() - async def shutdown(self): - """ - Gracefully shuts down the application and cleans up all resources. - """ + async def connect(self): + assert self._znp is None - self.close() + znp = ZNP(self.config) + await znp.connect() + + # We only assign `self._znp` after it has successfully connected + self._znp = znp + self._znp.set_application(self) + + self._bind_callbacks() + + async def disconnect(self): + self._reconnect_task.cancel() + self._watchdog_task.cancel() + + if self._znp is not None: + try: + await self._znp.reset(wait_for_reset=False) + except Exception as e: + LOGGER.warning("Failed to reset before disconnect: %s", e) + finally: + self._znp.close() + self._znp = None def close(self): self._reconnect_task.cancel() @@ -140,122 +138,67 @@ def close(self): self._znp.close() self._znp = None - async def startup(self, auto_form=False, force_form=False, read_only=False): + async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: """ - Performs application startup. - - This entails creating the ZNP object, connecting to the radio, potentially - forming a network, and configuring our settings. + Registers a new endpoint on the device. """ - try: - return await self._startup( - auto_form=auto_form, - force_form=force_form, - read_only=read_only, - ) - except Exception: - await self.shutdown() - raise - - async def _startup(self, auto_form=False, force_form=False, read_only=False): - assert self._znp is None + await self._znp.request( + c.AF.Register.Req( + Endpoint=descriptor.endpoint, + ProfileId=descriptor.profile, + DeviceId=descriptor.device_type, + DeviceVersion=descriptor.device_version, + LatencyReq=c.af.LatencyReq.NoLatencyReqs, + InputClusters=descriptor.input_clusters, + OutputClusters=descriptor.output_clusters, + ), + RspStatus=t.Status.SUCCESS, + ) - znp = ZNP(self.config) - await znp.connect() + async def load_network_info(self, *, load_devices=False) -> None: + """ + Loads network information from NVRAM. + """ - # We only assign `self._znp` after it has successfully connected - self._znp = znp - self._znp.set_application(self) + await self._znp.load_network_info(load_devices=load_devices) - if not read_only and not force_form: - await self._migrate_nvram() + self.state.node_info = self._znp.node_info + self.state.network_info = self._znp.network_info - self._bind_callbacks() + async def write_network_info( + self, + *, + network_info: zigpy.state.NetworkInfo, + node_info: zigpy.state.NodeInfo, + ) -> None: + """ + Writes network and node state to NVRAM. + """ - # Next, read out the NVRAM item that Zigbee2MQTT writes when it has configured - # a device to make sure that our network settings will not be reset. - if self._znp.version == 1.2: - configured_nv_item = OsalNvIds.HAS_CONFIGURED_ZSTACK1 - else: - configured_nv_item = OsalNvIds.HAS_CONFIGURED_ZSTACK3 + network_info.stack_specific.setdefault("zstack", {}) - try: - configured_value = await self._znp.nvram.osal_read( - configured_nv_item, item_type=t.uint8_t - ) - except KeyError: - is_configured = False - else: - is_configured = configured_value == const.ZSTACK_CONFIGURE_SUCCESS - - if force_form: - LOGGER.info("Forming a new network") - await self.form_network() - elif not is_configured: - if not auto_form: - raise RuntimeError("Cannot start application, network is not formed") - elif read_only: - raise RuntimeError( - "Cannot start application, network is not formed and read-only" - ) + if "tclk_seed" not in network_info.stack_specific["zstack"]: + network_info.stack_specific["zstack"]["tclk_seed"] = os.urandom(16).hex() - LOGGER.info("ZNP is not configured, forming a new network") + return await self._znp.write_network_info( + network_info=network_info, node_info=node_info + ) - # Network formation requires multiple resets so it will write the NVRAM - # settings itself - await self.form_network() - else: - # Issue a reset first to make sure we aren't permitting joins - await self._znp.reset() + async def start_network(self, *, read_only=False): + if self.state.node_info == zigpy.state.NodeInfo(): + await self.load_network_info() - LOGGER.info("ZNP is already configured, not forming a new network") + if not read_only: + await self._znp.migrate_nvram() + await self._write_stack_settings() - if not read_only: - await self._write_stack_settings(reset_if_changed=True) + await self._znp.reset() - # At this point the device state should the same, regardless of whether we just - # formed a new network or are restoring one if self.znp_config[conf.CONF_TX_POWER] is not None: await self.set_tx_power(dbm=self.znp_config[conf.CONF_TX_POWER]) - # Both versions of Z-Stack use this callback - started_as_coordinator = self._znp.wait_for_response( - c.ZDO.StateChangeInd.Callback(State=t.DeviceState.StartedAsCoordinator) - ) - - if self._znp.version == 1.2: - # Z-Stack Home 1.2 has a simple startup sequence - await self._znp.request( - c.ZDO.StartupFromApp.Req(StartDelay=100), - RspState=c.zdo.StartupState.RestoredNetworkState, - ) - else: - # Z-Stack 3 uses the BDB subsystem - bdb_commissioning_done = self._znp.wait_for_response( - c.AppConfig.BDBCommissioningNotification.Callback( - partial=True, RemainingModes=c.app_config.BDBCommissioningMode.NONE - ) - ) - - # According to the forums, this is the correct startup sequence, including - # the formation failure error - await self._znp.request_callback_rsp( - request=c.AppConfig.BDBStartCommissioning.Req( - Mode=c.app_config.BDBCommissioningMode.NwkFormation - ), - RspStatus=t.Status.SUCCESS, - callback=c.AppConfig.BDBCommissioningNotification.Callback( - partial=True, - Status=c.app_config.BDBCommissioningStatus.NetworkRestored, - ), - ) - - await bdb_commissioning_done - - # The startup sequence should not take forever - async with async_timeout.timeout(STARTUP_TIMEOUT): - await started_as_coordinator + await self._znp.start_network() self._version_rsp = await self._znp.request(c.SYS.Version.Req()) @@ -264,8 +207,7 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False): if self.znp_config[conf.CONF_LED_MODE] is not None: await self._set_led_mode(led=0xFF, mode=self.znp_config[conf.CONF_LED_MODE]) - await self.load_network_info() - await self._register_endpoints() + await self.register_endpoints() # Receive a callback for every known ZDO command for cluster_id in zdo_t.ZDOCmd: @@ -276,9 +218,10 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False): await self._znp.request(c.ZDO.MsgCallbackRegister.Req(ClusterId=cluster_id)) # Setup the coordinator as a zigpy device and initialize it to request node info - self.devices[self.ieee] = ZNPCoordinator(self, self.ieee, self.nwk) - self.zigpy_device.zdo.add_listener(self) - await self.zigpy_device.schedule_initialize() + self.devices[self.state.node_info.ieee] = ZNPCoordinator( + self, self.state.node_info.ieee, self.state.node_info.nwk + ) + await self._device.schedule_initialize() # Now that we know what device we are, set the max concurrent requests if self.znp_config[conf.CONF_MAX_CONCURRENT_REQUESTS] == "auto": @@ -288,35 +231,13 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False): self._concurrent_requests_semaphore = asyncio.Semaphore(max_concurrent_requests) - LOGGER.info("Network settings") - LOGGER.info(" Model: %s", self.zigpy_device.model) - LOGGER.info(" Z-Stack version: %s", self._znp.version) - LOGGER.info(" Z-Stack build id: %s", self._zstack_build_id) - LOGGER.info(" Max concurrent requests: %s", max_concurrent_requests) - LOGGER.info(" Channel: %s", self.channel) - LOGGER.info(" PAN ID: 0x%04X", self.pan_id) - LOGGER.info(" Extended PAN ID: %s", self.extended_pan_id) - LOGGER.info(" Device IEEE: %s", self.ieee) - LOGGER.info(" Device NWK: 0x%04X", self.nwk) - LOGGER.debug( - " Network key: %s", - ":".join( - f"{c:02x}" for c in self.state.network_information.network_key.key - ), - ) - - if self.state.network_information.network_key.key == const.Z2M_NETWORK_KEY: + if self.state.network_info.network_key.key == const.Z2M_NETWORK_KEY: LOGGER.warning( "Your network is using the insecure Zigbee2MQTT network key!" ) self._watchdog_task = asyncio.create_task(self._watchdog_loop()) - # XXX: The CC2531 running Z-Stack Home 1.2 permanently permits joins on startup - # unless they are explicitly disabled. We can't fix this but we can disable them - # as early as possible to shrink the window of opportunity for unwanted joins. - await self.permit(time_s=0) - async def set_tx_power(self, dbm: int) -> None: """ Sets the radio TX power. @@ -336,64 +257,6 @@ async def set_tx_power(self, dbm: int) -> None: "Requested TX power %d was adjusted to %d", dbm, rsp.StatusOrPower ) - async def form_network(self): - """ - Clears the current config and forms a new network with a random network key, - PAN ID, and extended PAN ID. - """ - - # First, make the settings consistent and randomly generate missing values - channel = self.config[conf.CONF_NWK][conf.CONF_NWK_CHANNEL] - channels = self.config[conf.CONF_NWK][conf.CONF_NWK_CHANNELS] - pan_id = self.config[conf.CONF_NWK][conf.CONF_NWK_PAN_ID] - extended_pan_id = self.config[conf.CONF_NWK][conf.CONF_NWK_EXTENDED_PAN_ID] - network_key = self.config[conf.CONF_NWK][conf.CONF_NWK_KEY] - - if pan_id is None: - pan_id = random.SystemRandom().randint(0x0001, 0xFFFE + 1) - - if extended_pan_id is None: - extended_pan_id = ExtendedPanId(os.urandom(8)) - - if network_key is None: - network_key = t.KeyData(os.urandom(16)) - - # Override `channels` with a single channel if one is explicitly set - if channel is not None: - channels = t.Channels.from_channel_list([channel]) - - network_info = zigpy.state.NetworkInformation( - extended_pan_id=extended_pan_id, - pan_id=pan_id, - nwk_update_id=self.config[conf.CONF_NWK][conf.CONF_NWK_UPDATE_ID], - nwk_manager_id=0x0000, - channel=channel, - channel_mask=channels, - security_level=5, - network_key=zigpy.state.Key( - key=network_key, - tx_counter=0, - rx_counter=0, - seq=0, - partner_ieee=None, - ), - tc_link_key=None, - children=[], - key_table=[], - nwk_addresses={}, - stack_specific={"zstack": {"tclk_seed": os.urandom(16).hex()}}, - ) - - node_info = zigpy.state.NodeInfo( - nwk=0x0000, - ieee=None, - logical_type=zdo_t.LogicalType.Coordinator, - ) - - await self.write_network_info(network_info=network_info, node_info=node_info) - await self._write_stack_settings(reset_if_changed=False) - await self._znp.reset() - def get_dst_address(self, cluster: zigpy.zcl.Cluster) -> zdo_t.MultiAddress: """ Helper to get a dst address for bind/unbind operations. @@ -404,7 +267,7 @@ def get_dst_address(self, cluster: zigpy.zcl.Cluster) -> zdo_t.MultiAddress: dst_addr = zdo_t.MultiAddress() dst_addr.addrmode = 0x03 - dst_addr.ieee = self.ieee + dst_addr.ieee = self.state.node_info.ieee dst_addr.endpoint = self._find_endpoint( dst_ep=cluster.endpoint.endpoint_id, profile=cluster.endpoint.profile_id, @@ -513,16 +376,22 @@ async def permit(self, time_s: int = 60, node: t.EUI64 = None): # through the coordinator itself. # # Fixed in https://github.com/Koenkk/Z-Stack-firmware/commit/efac5ee46b9b437 - if time_s == 0 or self._zstack_build_id < 20210708 or node == self.ieee: + if ( + time_s == 0 + or self._zstack_build_id < 20210708 + or node == self.state.node_info.ieee + ): response = await self._znp.request_callback_rsp( request=c.ZDO.MgmtPermitJoinReq.Req( AddrMode=t.AddrMode.NWK, - Dst=0x0000, + Dst=self.state.node_info.nwk, Duration=time_s, TCSignificance=1, ), RspStatus=t.Status.SUCCESS, - callback=c.ZDO.MgmtPermitJoinRsp.Callback(Src=0x0000, partial=True), + callback=c.ZDO.MgmtPermitJoinRsp.Callback( + Src=self.state.node_info.nwk, partial=True + ), ) if response.Status != t.Status.SUCCESS: @@ -537,6 +406,13 @@ async def permit_ncp(self, time_s: int) -> None: # Z-Stack does not need any special code to do this + async def force_remove(self, dev: zigpy.device.Device) -> None: + """ + Send a lower-level leave command to the device + """ + + # Z-Stack does not have any way to do this + async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): """ Permits a new device to join with the given IEEE and Install Code. @@ -776,8 +652,8 @@ async def on_af_message(self, msg: c.AF.IncomingMsg.Callback) -> None: device.radio_details(lqi=msg.LQI, rssi=None) # XXX: Is it possible to receive messages on non-assigned endpoints? - if msg.DstEndpoint in self.zigpy_device.endpoints: - profile = self.zigpy_device.endpoints[msg.DstEndpoint].profile_id + if msg.DstEndpoint in self._device.endpoints: + profile = self._device.endpoints[msg.DstEndpoint].profile_id else: LOGGER.warning("Received a message on an unregistered endpoint: %s", msg) profile = zigpy.profiles.zha.PROFILE_ID @@ -807,14 +683,6 @@ def _zstack_build_id(self) -> t.uint32_t: return self._version_rsp.CodeRevision - @property - def zigpy_device(self) -> zigpy.device.Device: - """ - Reference to zigpy device 0x0000, the coordinator. - """ - - return self.devices[self.ieee] - @property def znp_config(self) -> conf.ConfigType: """ @@ -925,59 +793,7 @@ async def _set_led_mode(self, *, led: t.uint8_t, mode: c.util.LEDMode) -> None: except (asyncio.TimeoutError, CommandNotRecognized): LOGGER.info("This build of Z-Stack does not appear to support LED control") - async def _migrate_nvram(self): - """ - Migrates NVRAM entries using the `ZIGPY_ZNP_MIGRATION_ID` NVRAM item. - """ - - try: - migration_id = await self._znp.nvram.osal_read( - OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, item_type=t.uint8_t - ) - except KeyError: - migration_id = 0 - - initial_migration_id = migration_id - - # Migration 1: empty `ADDRMGR` entries are version-dependent and were improperly - # written for CC253x devices. - # - # This migration is stateless and can safely be run more than once: - # the only downside is that startup times increase by 10s on newer - # coordinators, which is why the migration ID is persisted. - if migration_id < 1: - try: - entries = await security.read_addr_manager_entries(self._znp) - except KeyError: - pass - else: - fixed_entries = [] - - for entry in entries: - if entry.extAddr != t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"): - fixed_entries.append(entry) - elif self._znp.version == 3.30: - fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3) - else: - fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1) - - if entries != fixed_entries: - LOGGER.warning( - "Repairing %d invalid empty address manager entries (total %d)", - sum(i != j for i, j in zip(entries, fixed_entries)), - len(entries), - ) - await security.write_addr_manager_entries(self._znp, fixed_entries) - - migration_id = 1 - - if initial_migration_id != migration_id: - await self._znp.nvram.osal_write( - OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, t.uint8_t(migration_id), create=True - ) - await self._znp.reset() - - async def _write_stack_settings(self, *, reset_if_changed: bool) -> None: + async def _write_stack_settings(self) -> bool: """ Writes network-independent Z-Stack settings to NVRAM. If no settings actually change, no reset will be performed. @@ -985,7 +801,7 @@ async def _write_stack_settings(self, *, reset_if_changed: bool) -> None: # It's better to be explicit than rely on the NVRAM defaults settings = { - OsalNvIds.LOGICAL_TYPE: t.DeviceLogicalType.Coordinator, + OsalNvIds.LOGICAL_TYPE: t.uint8_t(self.state.node_info.logical_type), # Source routing OsalNvIds.CONCENTRATOR_ENABLE: t.Bool(True), OsalNvIds.CONCENTRATOR_DISCOVERY: t.uint8_t(120), @@ -1013,9 +829,7 @@ async def _write_stack_settings(self, *, reset_if_changed: bool) -> None: await self._znp.nvram.osal_write(nvid, value) any_changed = True - if reset_if_changed and any_changed: - # Reset to make the above NVRAM writes take effect - await self._znp.reset() + return any_changed @contextlib.asynccontextmanager async def _limit_concurrency(self): @@ -1069,7 +883,8 @@ async def _reconnect(self) -> None: ) try: - await self._startup() + await self.connect() + await self.start_network() return except asyncio.CancelledError: raise @@ -1086,67 +901,6 @@ async def _reconnect(self) -> None: ] ) - async def _register_endpoints(self) -> None: - """ - Registers the Zigbee endpoints required to communicate with various devices. - """ - - await self._znp.request( - c.AF.Register.Req( - Endpoint=ZHA_ENDPOINT, - ProfileId=zigpy.profiles.zha.PROFILE_ID, - DeviceId=zigpy.profiles.zha.DeviceType.IAS_CONTROL, - DeviceVersion=0b0000, - LatencyReq=c.af.LatencyReq.NoLatencyReqs, - InputClusters=[ - clusters.general.Basic.cluster_id, - clusters.general.Ota.cluster_id, - ], - OutputClusters=[ - clusters.security.IasZone.cluster_id, - clusters.security.IasWd.cluster_id, - ], - ), - RspStatus=t.Status.SUCCESS, - ) - - await self._znp.request( - c.AF.Register.Req( - Endpoint=ZLL_ENDPOINT, - ProfileId=zigpy.profiles.zll.PROFILE_ID, - DeviceId=zigpy.profiles.zll.DeviceType.CONTROLLER, - DeviceVersion=0b0000, - LatencyReq=c.af.LatencyReq.NoLatencyReqs, - InputClusters=[clusters.general.Basic.cluster_id], - OutputClusters=[], - ), - RspStatus=t.Status.SUCCESS, - ) - - async def load_network_info(self, *, load_devices=False) -> None: - """ - Loads network information from NVRAM. - """ - - await self._znp.load_network_info(load_devices=load_devices) - - self.state.node_information = self._znp.node_info - self.state.network_information = self._znp.network_info - - async def write_network_info( - self, - *, - network_info: zigpy.state.NetworkInformation, - node_info: zigpy.state.NodeInfo, - ) -> None: - """ - Writes network and node state to NVRAM. - """ - - return await self._znp.write_network_info( - network_info=network_info, node_info=node_info - ) - def _find_endpoint(self, dst_ep: int, profile: int, cluster: int) -> int: """ Zigpy defaults to sending messages with src_ep == dst_ep. This does not work @@ -1164,7 +918,7 @@ def _find_endpoint(self, dst_ep: int, profile: int, cluster: int) -> int: # Always fall back to endpoint 1 candidates = [ZHA_ENDPOINT] - for ep_id, endpoint in self.zigpy_device.endpoints.items(): + for ep_id, endpoint in self._device.endpoints.items(): if ep_id == ZDO_ENDPOINT: continue @@ -1238,7 +992,7 @@ async def _send_request_raw( if cluster == zdo_t.ZDOCmd.Mgmt_Permit_Joining_req: if dst_addr.mode == t.AddrMode.Broadcast: # The coordinator responds to broadcasts - permit_addr = 0x0000 + permit_addr = self.state.node_info.nwk else: # Otherwise, the destination device responds permit_addr = dst_addr.address @@ -1269,10 +1023,10 @@ async def _send_request_raw( ) or ( # Or a direct unicast request dst_addr.mode == t.AddrMode.NWK - and dst_addr.address == self.zigpy_device.nwk + and dst_addr.address == self._device.nwk ): self.handle_message( - sender=self.zigpy_device, + sender=self._device, profile=profile, cluster=cluster, src_ep=src_ep, @@ -1388,7 +1142,7 @@ async def _send_request( if ( dst_ep == ZDO_ENDPOINT and dst_addr.mode == t.AddrMode.NWK - and dst_addr.address != 0x0000 + and dst_addr.address != self.state.node_info.nwk ): route_status = await self._znp.request( c.ZDO.ExtRouteChk.Req( diff --git a/zigpy_znp/zigbee/device.py b/zigpy_znp/zigbee/device.py index d93f02e3..dc1eeaa2 100644 --- a/zigpy_znp/zigbee/device.py +++ b/zigpy_znp/zigbee/device.py @@ -93,7 +93,7 @@ def _send_loopback_reply( LOGGER.debug("Sending loopback reply %s (%s), tsn=%s", command_id, kwargs, tsn) self.app.handle_message( - sender=self.app.zigpy_device, + sender=self.app._device, profile=znp_app.ZDO_PROFILE, cluster=command_id, src_ep=znp_app.ZDO_ENDPOINT, @@ -124,7 +124,7 @@ async def async_handle_mgmt_nwk_update_req( ): return - old_network_info = self.app.state.network_information + old_network_info = self.app.state.network_info if ( t.Channels.from_channel_list([old_network_info.channel]) @@ -157,18 +157,17 @@ async def async_handle_mgmt_nwk_update_req( # Wait until the network info changes, it can take ~5s while ( - self.app.state.network_information.nwk_update_id - == old_network_info.nwk_update_id + self.app.state.network_info.nwk_update_id == old_network_info.nwk_update_id ): await self.app.load_network_info(load_devices=False) await asyncio.sleep(NWK_UPDATE_LOOP_DELAY) # Z-Stack automatically increments the NWK update ID instead of setting it # TODO: Directly set it once radio settings API is finalized. - if NwkUpdate.nwkUpdateId != self.app.state.network_information.nwk_update_id: + if NwkUpdate.nwkUpdateId != self.app.state.network_info.nwk_update_id: LOGGER.warning( f"`nwkUpdateId` was incremented to" - f" {self.app.state.network_information.nwk_update_id} instead of being" + f" {self.app.state.network_info.nwk_update_id} instead of being" f" set to {NwkUpdate.nwkUpdateId}" ) diff --git a/zigpy_znp/znp/security.py b/zigpy_znp/znp/security.py index 08b75cc1..8c4feb92 100644 --- a/zigpy_znp/znp/security.py +++ b/zigpy_znp/znp/security.py @@ -71,7 +71,7 @@ def iter_seed_candidates( yield count, tclk_seed -async def read_tc_frame_counter(znp: ZNP, *, ext_pan_id: t.EUI64 = None) -> t.uint32_t: +async def read_nwk_frame_counter(znp: ZNP, *, ext_pan_id: t.EUI64 = None) -> t.uint32_t: if ext_pan_id is None and znp.network_info is not None: ext_pan_id = znp.network_info.extended_pan_id @@ -110,7 +110,7 @@ async def read_tc_frame_counter(znp: ZNP, *, ext_pan_id: t.EUI64 = None) -> t.ui return global_entry.FrameCounter -async def write_tc_frame_counter( +async def write_nwk_frame_counter( znp: ZNP, counter: t.uint32_t, *, ext_pan_id: t.EUI64 = None ) -> None: if znp.version == 1.2: