From a3397890ffe451b0e784f1f800efd2e57157a9aa Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 10:14:20 +0400 Subject: [PATCH 01/57] add tests for uart module --- requirements_test.txt | 5 + tests/__init__.py | 0 tests/test_uart.py | 217 ++++++++++++++++++++++++++++++++++++++++++ zigpy_zboss/uart.py | 3 + 4 files changed, 225 insertions(+) create mode 100644 requirements_test.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_uart.py diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..82c2e0e --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,5 @@ +pytest>=7.3.1 +pytest-asyncio>=0.21.0 +pytest-timeout>=2.1.0 +pytest-mock>=3.10.0 +pytest-cov>=4.1.0 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uart.py b/tests/test_uart.py new file mode 100644 index 0000000..7dea5c2 --- /dev/null +++ b/tests/test_uart.py @@ -0,0 +1,217 @@ +import pytest +from serial_asyncio import SerialTransport + +import zigpy_zboss.config as conf +import zigpy_zboss.commands as c +from zigpy_zboss import uart as znp_uart +from zigpy_zboss.frames import Frame +from zigpy_zboss.checksum import CRC8 + + +@pytest.fixture +def connected_uart(mocker): + znp = mocker.Mock() + config = { + conf.CONF_DEVICE_PATH: "/dev/ttyACM0", + conf.CONF_DEVICE_BAUDRATE: 115200, + conf.CONF_DEVICE_FLOW_CONTROL: None} + + uart = znp_uart.ZbossNcpProtocol(config, znp) + uart.connection_made(mocker.Mock()) + + yield znp, uart + + +def ll_checksum(frame): + """Return frame with new crc8 checksum calculation.""" + crc = CRC8(frame.ll_header.serialize()[2:6]).digest() + frame.ll_header = frame.ll_header.with_crc8(crc) + return frame + + +@pytest.fixture +def dummy_serial_conn(event_loop, mocker): + device = "/dev/ttyACM0" + + serial_interface = mocker.Mock() + serial_interface.name = device + + def create_serial_conn(loop, protocol_factory, url, *args, **kwargs): + fut = event_loop.create_future() + assert url == device + + protocol = protocol_factory() + + # Our event loop doesn't really do anything + event_loop.add_writer = lambda *args, **kwargs: None + event_loop.add_reader = lambda *args, **kwargs: None + event_loop.remove_writer = lambda *args, **kwargs: None + event_loop.remove_reader = lambda *args, **kwargs: None + + transport = SerialTransport(event_loop, protocol, serial_interface) + + protocol.connection_made(transport) + + fut.set_result((transport, protocol)) + + return fut + + mocker.patch("serial_asyncio.create_serial_connection", new=create_serial_conn) + + return device, serial_interface + + +def test_uart_rx_basic(connected_uart): + znp, uart = connected_uart + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + uart.data_received(test_frame_bytes) + + znp.frame_received.assert_called_once_with(test_frame) + + +def test_uart_str_repr(connected_uart): + znp, uart = connected_uart + + str(uart) + repr(uart) + + +def test_uart_rx_byte_by_byte(connected_uart): + znp, uart = connected_uart + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + for byte in test_frame_bytes: + uart.data_received(bytes([byte])) + + znp.frame_received.assert_called_once_with(test_frame) + + +def test_uart_rx_byte_by_byte_garbage(connected_uart): + znp, uart = connected_uart + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + data = b"" + data += bytes.fromhex("58 4a 72 35 51 da 60 ed 1f") + data += bytes.fromhex("03 6d b6") + data += bytes.fromhex("ee 90") + data += test_frame_bytes + data += bytes.fromhex("00 00") + data += bytes.fromhex("e4 4f 51 b2 39 4b 8d e3 ca 61") + data += bytes.fromhex("8c 56 8a 2c d8 22 64 9e 9d 7b") + + # The frame should be parsed identically regardless of framing + for byte in data: + uart.data_received(bytes([byte])) + + znp.frame_received.assert_called_once_with(test_frame) + + +def test_uart_rx_big_garbage(connected_uart): + znp, uart = connected_uart + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + data = b"" + data += bytes.fromhex("58 4a 72 35 51 da 60 ed 1f") + data += bytes.fromhex("03 6d b6") + data += bytes.fromhex("ee 90") + data += test_frame_bytes + data += bytes.fromhex("00 00") + data += bytes.fromhex("e4 4f 51 b2 39 4b 8d e3 ca 61") + data += bytes.fromhex("8c 56 8a 2c d8 22 64 9e 9d 7b") + + # The frame should be parsed identically regardless of framing + uart.data_received(data) + + znp.frame_received.assert_called_once_with(test_frame) + + +def test_uart_rx_corrupted_fcs(connected_uart): + znp, uart = connected_uart + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + # Almost, but not quite + uart.data_received(test_frame_bytes[:-1]) + uart.data_received(b"\x00") + + assert not znp.frame_received.called + + +def test_uart_rx_sof_stress(connected_uart): + znp, uart = connected_uart + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + # We include an almost-valid frame and many stray SoF markers + uart.data_received(b"\xFE" + b"\xFE" + b"\xFE" + test_frame_bytes[:-1] + b"\x00") + uart.data_received(b"\xFE\xFE\x00\xFE\x01") + uart.data_received(b"\xFE" + b"\xFE" + b"\xFE" + test_frame_bytes + b"\x00\x00") + + # We should see the valid frame exactly once + znp.frame_received.assert_called_once_with(test_frame) + + +def test_uart_frame_received_error(connected_uart, mocker): + znp, uart = connected_uart + znp.frame_received = mocker.Mock(side_effect=RuntimeError("An error")) + + test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_frame = test_command.to_frame() + test_frame = ll_checksum(test_frame) + test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + + # Errors thrown by znp.frame_received should not impact how many frames are handled + uart.data_received(test_frame_bytes * 3) + + # We should have received all three frames + assert znp.frame_received.call_count == 3 + +@pytest.mark.asyncio +async def test_connection_lost(dummy_serial_conn, mocker, event_loop): + device, _ = dummy_serial_conn + + znp = mocker.Mock() + conn_lost_fut = event_loop.create_future() + znp.connection_lost = conn_lost_fut.set_result + + protocol = await znp_uart.connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=znp + ) + + exception = RuntimeError("Uh oh, something broke") + protocol.connection_lost(exception) + + # Losing a connection propagates up to the ZNP object + assert (await conn_lost_fut) == exception + +@pytest.mark.asyncio +async def test_connection_made(dummy_serial_conn, mocker): + device, _ = dummy_serial_conn + znp = mocker.Mock() + + await znp_uart.connect(conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=znp) + + znp.connection_made.assert_called_once_with() \ No newline at end of file diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index 66e30db..35914ba 100644 --- a/zigpy_zboss/uart.py +++ b/zigpy_zboss/uart.py @@ -80,6 +80,9 @@ def connection_made( SERIAL_LOGGER.info(message) self._connected_event.set() + if self._api is not None: + self._api.connection_made() + def connection_lost(self, exc: typing.Optional[Exception]) -> None: """Lost connection.""" if self._api is not None: From 3f629f170f6a5d41da780006991bc83c0d8c3989 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 10:22:24 +0400 Subject: [PATCH 02/57] add flake8 --- requirements_test.txt | 3 ++- tests/test_uart.py | 51 ++++++++++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 82c2e0e..85c2944 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,4 +2,5 @@ pytest>=7.3.1 pytest-asyncio>=0.21.0 pytest-timeout>=2.1.0 pytest-mock>=3.10.0 -pytest-cov>=4.1.0 \ No newline at end of file +pytest-cov>=4.1.0 +flake8==5.0.4 \ No newline at end of file diff --git a/tests/test_uart.py b/tests/test_uart.py index 7dea5c2..2bc35b5 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -56,7 +56,9 @@ def create_serial_conn(loop, protocol_factory, url, *args, **kwargs): return fut - mocker.patch("serial_asyncio.create_serial_connection", new=create_serial_conn) + mocker.patch( + "serial_asyncio.create_serial_connection", new=create_serial_conn + ) return device, serial_interface @@ -67,7 +69,9 @@ def test_uart_rx_basic(connected_uart): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() uart.data_received(test_frame_bytes) @@ -87,7 +91,9 @@ def test_uart_rx_byte_by_byte(connected_uart): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() for byte in test_frame_bytes: uart.data_received(bytes([byte])) @@ -101,7 +107,9 @@ def test_uart_rx_byte_by_byte_garbage(connected_uart): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() data = b"" data += bytes.fromhex("58 4a 72 35 51 da 60 ed 1f") @@ -125,7 +133,9 @@ def test_uart_rx_big_garbage(connected_uart): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() data = b"" data += bytes.fromhex("58 4a 72 35 51 da 60 ed 1f") @@ -148,7 +158,9 @@ def test_uart_rx_corrupted_fcs(connected_uart): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() # Almost, but not quite uart.data_received(test_frame_bytes[:-1]) @@ -163,12 +175,18 @@ def test_uart_rx_sof_stress(connected_uart): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() # We include an almost-valid frame and many stray SoF markers - uart.data_received(b"\xFE" + b"\xFE" + b"\xFE" + test_frame_bytes[:-1] + b"\x00") + uart.data_received( + b"\xFE" + b"\xFE" + b"\xFE" + test_frame_bytes[:-1] + b"\x00" + ) uart.data_received(b"\xFE\xFE\x00\xFE\x01") - uart.data_received(b"\xFE" + b"\xFE" + b"\xFE" + test_frame_bytes + b"\x00\x00") + uart.data_received( + b"\xFE" + b"\xFE" + b"\xFE" + test_frame_bytes + b"\x00\x00" + ) # We should see the valid frame exactly once znp.frame_received.assert_called_once_with(test_frame) @@ -181,14 +199,18 @@ def test_uart_frame_received_error(connected_uart, mocker): test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) - test_frame_bytes = Frame(test_frame.ll_header, test_frame.hl_packet).serialize() + test_frame_bytes = Frame( + test_frame.ll_header, test_frame.hl_packet + ).serialize() - # Errors thrown by znp.frame_received should not impact how many frames are handled + # Errors thrown by znp.frame_received should + # not impact how many frames are handled uart.data_received(test_frame_bytes * 3) # We should have received all three frames assert znp.frame_received.call_count == 3 + @pytest.mark.asyncio async def test_connection_lost(dummy_serial_conn, mocker, event_loop): device, _ = dummy_serial_conn @@ -207,11 +229,14 @@ async def test_connection_lost(dummy_serial_conn, mocker, event_loop): # Losing a connection propagates up to the ZNP object assert (await conn_lost_fut) == exception + @pytest.mark.asyncio async def test_connection_made(dummy_serial_conn, mocker): device, _ = dummy_serial_conn znp = mocker.Mock() - await znp_uart.connect(conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=znp) + await znp_uart.connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=znp + ) - znp.connection_made.assert_called_once_with() \ No newline at end of file + znp.connection_made.assert_called_once_with() From 952c201b7a8e9310f642a539fa5e8ad5001adb77 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 11:43:28 +0400 Subject: [PATCH 03/57] tests for utils --- tests/test_utils.py | 108 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..01cdeea --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,108 @@ +import asyncio + +import pytest + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c +from zigpy_zboss.utils import deduplicate_commands + + +def test_command_deduplication_simple(): + c1 = c.NcpConfig.GetModuleVersion.Req(TSN=10) + c2 = c.NcpConfig.NCPModuleReset.Req(TSN=10,Option=t.ResetOptions(0)) + + assert deduplicate_commands([]) == () + assert deduplicate_commands([c1]) == (c1,) + assert deduplicate_commands([c1, c1]) == (c1,) + assert deduplicate_commands([c1, c2]) == (c1, c2) + assert deduplicate_commands([c2, c1, c2]) == (c2, c1) + + +def test_command_deduplication_complex(): + result = deduplicate_commands( + [ + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3, + ), + # Duplicating matching commands shouldn't do anything + c.NcpConfig.GetModuleVersion.Rsp(partial=True), + c.NcpConfig.GetModuleVersion.Rsp(partial=True), + # Matching against different command types should also work + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=10, + DeviceRole=t.DeviceRole(2) + ), + c.NcpConfig.GetNwkKeys.Rsp( + partial=True, + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=10, + KeyNumber1=10, + ), + c.NcpConfig.GetNwkKeys.Rsp( + partial=True, + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=10, + KeyNumber1=10, + KeyNumber2=20, + ), + c.NcpConfig.GetNwkKeys.Rsp( + partial=True, + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=10, + KeyNumber1=10, + KeyNumber2=20, + KeyNumber3=30, + ), + c.NcpConfig.GetNwkKeys.Rsp( + partial=True, + TSN=11, + StatusCat=t.StatusCategory(2), + KeyNumber3=30, + ), + ] + ) + + assert set(result) == { + c.NcpConfig.GetModuleVersion.Rsp(partial=True), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=10, + DeviceRole=t.DeviceRole(2) + ), + c.NcpConfig.GetNwkKeys.Rsp( + partial=True, + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=10, + KeyNumber1=10, + ), + c.NcpConfig.GetNwkKeys.Rsp( + partial=True, + TSN=11, + StatusCat=t.StatusCategory(2), + KeyNumber3=30, + ), + } \ No newline at end of file From 577ef7457c20c4a4a816eb2f4cf00602f0ac2af9 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 11:46:20 +0400 Subject: [PATCH 04/57] flake8 --- tests/test_utils.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 01cdeea..b54f2e1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,3 @@ -import asyncio - -import pytest - import zigpy_zboss.types as t import zigpy_zboss.commands as c from zigpy_zboss.utils import deduplicate_commands @@ -9,7 +5,7 @@ def test_command_deduplication_simple(): c1 = c.NcpConfig.GetModuleVersion.Req(TSN=10) - c2 = c.NcpConfig.NCPModuleReset.Req(TSN=10,Option=t.ResetOptions(0)) + c2 = c.NcpConfig.NCPModuleReset.Req(TSN=10, Option=t.ResetOptions(0)) assert deduplicate_commands([]) == () assert deduplicate_commands([c1]) == (c1,) @@ -105,4 +101,4 @@ def test_command_deduplication_complex(): StatusCat=t.StatusCategory(2), KeyNumber3=30, ), - } \ No newline at end of file + } From 851b7ba52cd3b5bfd5a65dfc631144116d538542 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 11:54:25 +0400 Subject: [PATCH 05/57] use response commands in uart tests --- tests/test_uart.py | 50 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/tests/test_uart.py b/tests/test_uart.py index 2bc35b5..f2db4b7 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -3,6 +3,7 @@ import zigpy_zboss.config as conf import zigpy_zboss.commands as c +import zigpy_zboss.types as t from zigpy_zboss import uart as znp_uart from zigpy_zboss.frames import Frame from zigpy_zboss.checksum import CRC8 @@ -66,7 +67,12 @@ def create_serial_conn(loop, protocol_factory, url, *args, **kwargs): def test_uart_rx_basic(connected_uart): znp, uart = connected_uart - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( @@ -88,7 +94,12 @@ def test_uart_str_repr(connected_uart): def test_uart_rx_byte_by_byte(connected_uart): znp, uart = connected_uart - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( @@ -104,7 +115,12 @@ def test_uart_rx_byte_by_byte(connected_uart): def test_uart_rx_byte_by_byte_garbage(connected_uart): znp, uart = connected_uart - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( @@ -130,7 +146,12 @@ def test_uart_rx_byte_by_byte_garbage(connected_uart): def test_uart_rx_big_garbage(connected_uart): znp, uart = connected_uart - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( @@ -155,7 +176,12 @@ def test_uart_rx_big_garbage(connected_uart): def test_uart_rx_corrupted_fcs(connected_uart): znp, uart = connected_uart - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( @@ -172,7 +198,12 @@ def test_uart_rx_corrupted_fcs(connected_uart): def test_uart_rx_sof_stress(connected_uart): znp, uart = connected_uart - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( @@ -196,7 +227,12 @@ def test_uart_frame_received_error(connected_uart, mocker): znp, uart = connected_uart znp.frame_received = mocker.Mock(side_effect=RuntimeError("An error")) - test_command = c.NcpConfig.GetModuleVersion.Req(TSN=10) + test_command = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) test_frame = test_command.to_frame() test_frame = ll_checksum(test_frame) test_frame_bytes = Frame( From 8a0c1c6f38bf91bd5623d87dd904dc7f1f679428 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 12:07:28 +0400 Subject: [PATCH 06/57] tests for config module --- tests/test_config.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_config.py diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..3f4c084 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,57 @@ +import pytest +from voluptuous import Invalid + +import zigpy_zboss.config as conf + + +def test_pin_states_same_lengths(): + # Bare schema works + conf.CONFIG_SCHEMA( + { + conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: "/dev/null"}, + } + ) + + # So does one with explicitly specified pin states + config = conf.CONFIG_SCHEMA( + { + conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: "/dev/null"}, + conf.CONF_ZBOSS_CONFIG: { + conf.CONF_CONNECT_RTS_STATES: ["on", True, 0, 0, 0, 1, 1], + conf.CONF_CONNECT_DTR_STATES: ["off", False, 1, 0, 0, 1, 1], + }, + } + ) + + assert config[conf.CONF_ZBOSS_CONFIG][conf.CONF_CONNECT_RTS_STATES] == [ + True, + True, + False, + False, + False, + True, + True, + ] + assert config[conf.CONF_ZBOSS_CONFIG][conf.CONF_CONNECT_DTR_STATES] == [ + False, + False, + True, + False, + False, + True, + True, + ] + + +def test_pin_states_different_lengths(): + # They must be the same length + with pytest.raises(Invalid): + conf.CONFIG_SCHEMA( + { + conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: "/dev/null"}, + conf.CONF_ZBOSS_CONFIG: { + conf.CONF_CONNECT_RTS_STATES: [1, 1, 0], + conf.CONF_CONNECT_DTR_STATES: [1, 1], + }, + } + ) From d4a17e8bdf4b381d4d999995d3444ed10de955d5 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 12:45:23 +0400 Subject: [PATCH 07/57] test types basic --- tests/test_types_basic.py | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 tests/test_types_basic.py diff --git a/tests/test_types_basic.py b/tests/test_types_basic.py new file mode 100644 index 0000000..bec532f --- /dev/null +++ b/tests/test_types_basic.py @@ -0,0 +1,166 @@ +import pytest + +import zigpy_zboss.types as t + + +def test_enum(): + class TestEnum(t.bitmap16): + ALL = 0xFFFF + CH_1 = 0x0001 + CH_2 = 0x0002 + CH_3 = 0x0004 + CH_5 = 0x0008 + CH_6 = 0x0010 + CH_Z = 0x8000 + + extra = b"The rest of the data\x55\xaa" + data = b"\x12\x80" + + r, rest = TestEnum.deserialize(data + extra) + assert rest == extra + assert r == 0x8012 + assert r == (TestEnum.CH_2 | TestEnum.CH_6 | TestEnum.CH_Z) + + assert r.serialize() == data + assert TestEnum(0x8012).serialize() == data + + +def test_int_too_short(): + with pytest.raises(ValueError): + t.uint8_t.deserialize(b"") + + with pytest.raises(ValueError): + t.uint16_t.deserialize(b"\x00") + + +def test_int_out_of_bounds(): + with pytest.raises(ValueError): + t.uint8_t(-1) + + with pytest.raises(ValueError): + t.uint8_t(0xFF + 1) + + +def test_bytes(): + data = b"abcde\x00\xff" + + r, rest = t.Bytes.deserialize(data) + assert rest == b"" + assert r == data + + assert r.serialize() == data + + assert str(r) == repr(r) == "b'61:62:63:64:65:00:FF'" + + # Ensure we don't make any mistakes formatting the bytes + all_bytes = t.Bytes(bytes(range(0, 255 + 1))) + assert str(all_bytes) == ( + "b'00:01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:" + "0F:10:11:12:13:14:15:16:17:18:19:1A:1B:1C:1D:1E" + ":1F:20:21:22:23:24:25:26:27:28:29:2A:2B:2C:2D:2" + "E:2F:30:31:32:33:34:35:36:37:38:39:3A:3B:3C:3D:" + "3E:3F:40:41:42:43:44:45:46:47:48:49:4A:4B:4C:4D" + ":4E:4F:50:51:52:53:54:55:56:57:58:59:5A:5B:5C:5" + "D:5E:5F:60:61:62:63:64:65:66:67:68:69:6A:6B:6C:" + "6D:6E:6F:70:71:72:73:74:75:76:77:78:79:7A:7B:7C" + ":7D:7E:7F:80:81:82:83:84:85:86:87:88:89:8A:8B:8" + "C:8D:8E:8F:90:91:92:93:94:95:96:97:98:99:9A:9B:" + "9C:9D:9E:9F:A0:A1:A2:A3:A4:A5:A6:A7:A8:A9:AA:AB" + ":AC:AD:AE:AF:B0:B1:B2:B3:B4:B5:B6:B7:B8:B9:BA:" + "BB:BC:BD:BE:BF:C0:C1:C2:C3:C4:C5:C6:C7:C8:C9:CA" + ":CB:CC:CD:CE:CF:D0:D1:D2:D3:D4:D5:D6:D7:D8:D9:" + "DA:DB:DC:DD:DE:DF:E0:E1:E2:E3:E4:E5:E6:E7:E8:" + "E9:EA:EB:EC:ED:EE:EF:F0:F1:F2:F3:F4:F5:F6:" + "F7:F8:F9:FA:FB:FC:FD:FE:FF'" + ) + + +def test_longbytes(): + data = b"abcde\x00\xff" * 50 + extra = b"\xffrest of the data\x00" + + r, rest = t.LongBytes.deserialize( + len(data).to_bytes(2, "little") + data + extra + ) + assert rest == extra + assert r == data + + assert r.serialize() == len(data).to_bytes( + 2, "little" + ) + data + + with pytest.raises(ValueError): + t.LongBytes.deserialize(b"\x01") + + with pytest.raises(ValueError): + t.LongBytes.deserialize(b"\x01\x00") + + with pytest.raises(ValueError): + t.LongBytes.deserialize( + len(data).to_bytes(2, "little") + data[:-1] + ) + + +def test_lvlist(): + class TestList(t.LVList, item_type=t.uint8_t, length_type=t.uint8_t): + pass + + d, r = TestList.deserialize(b"\x0412345") + assert r == b"5" + assert d == list(map(ord, "1234")) + assert TestList.serialize(d) == b"\x041234" + + assert isinstance(d, TestList) + + with pytest.raises(ValueError): + TestList([1, 2, 0xFFFF, 4]).serialize() + + +def test_lvlist_too_short(): + class TestList(t.LVList, item_type=t.uint8_t, length_type=t.uint8_t): + pass + + with pytest.raises(ValueError): + TestList.deserialize(b"") + + with pytest.raises(ValueError): + TestList.deserialize(b"\x04123") + + +def test_fixed_list(): + class TestList(t.FixedList, item_type=t.uint16_t, length=3): + pass + + with pytest.raises(ValueError): + r = TestList([1, 2, 3, 0x55AA]) + r.serialize() + + with pytest.raises(ValueError): + r = TestList([1, 2]) + r.serialize() + + r = TestList([1, 2, 3]) + + assert r.serialize() == b"\x01\x00\x02\x00\x03\x00" + + +def test_fixed_list_deserialize(): + class TestList(t.FixedList, length=3, item_type=t.uint16_t): + pass + + data = b"\x34\x12\x55\xaa\x89\xab" + extra = b"\x00\xff" + + r, rest = TestList.deserialize(data + extra) + assert rest == extra + assert r[0] == 0x1234 + assert r[1] == 0xAA55 + assert r[2] == 0xAB89 + + +def test_enum_instance_types(): + class TestEnum(t.enum8): + Member = 0x00 + + assert TestEnum._member_type_ is t.uint8_t + assert type(TestEnum.Member.value) is t.uint8_t From 3bc7ea1152359e0c5803cdaada9b42f319b8dd8d Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 10 May 2024 13:09:33 +0400 Subject: [PATCH 08/57] tests for cstruct types --- tests/test_types_cstruct.py | 279 ++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 tests/test_types_cstruct.py diff --git a/tests/test_types_cstruct.py b/tests/test_types_cstruct.py new file mode 100644 index 0000000..a1ddecb --- /dev/null +++ b/tests/test_types_cstruct.py @@ -0,0 +1,279 @@ +import pytest + +import zigpy_zboss.types as t + + +def test_struct_fields(): + class TestStruct(t.CStruct): + a: t.uint8_t + b: t.uint16_t + + assert len(TestStruct.fields) == 2 + + assert TestStruct.fields.a.name == "a" + assert TestStruct.fields.a.type == t.uint8_t + + assert TestStruct.fields.b.name == "b" + assert TestStruct.fields.b.type == t.uint16_t + + +def test_struct_field_values(): + class TestStruct(t.CStruct): + a: t.uint8_t + b: t.uint16_t + + struct = TestStruct(a=1, b=2) + assert struct.a == 1 + assert isinstance(struct.a, t.uint8_t) + + assert struct.b == 2 + assert isinstance(struct.b, t.uint16_t) + + # Invalid values can't be passed during construction + with pytest.raises(ValueError): + TestStruct(a=1, b=2**32) + + struct2 = TestStruct() + struct2.a = 1 + struct2.b = 2 + + assert struct == struct2 + assert struct.serialize() == struct2.serialize() + + +def test_struct_methods_and_constants(): + class TestStruct(t.CStruct): + a: t.uint8_t + b: t.uint16_t + + def method(self): + return self.a + self.b + + def annotated_method(self: "TestStruct") -> int: + return self.method() + + CONSTANT1 = 1 + constant2 = "foo" + _constant3 = "bar" + + assert len(TestStruct.fields) == 2 + assert TestStruct.fields.a == t.CStructField(name="a", type=t.uint8_t) + assert TestStruct.fields.b == t.CStructField(name="b", type=t.uint16_t) + + assert TestStruct.CONSTANT1 == 1 + assert TestStruct.constant2 == "foo" + assert TestStruct._constant3 == "bar" + + assert TestStruct(a=1, b=2).method() == 3 + + +def test_struct_nesting(): + class Outer(t.CStruct): + e: t.uint32_t + + class TestStruct(t.CStruct): + class Inner(t.CStruct): + c: t.uint16_t + + a: t.uint8_t + b: Inner + d: Outer + + assert len(TestStruct.fields) == 3 + assert TestStruct.fields.a == t.CStructField(name="a", type=t.uint8_t) + assert TestStruct.fields.b == t.CStructField( + name="b", type=TestStruct.Inner + ) + assert TestStruct.fields.d == t.CStructField(name="d", type=Outer) + + assert len(TestStruct.Inner.fields) == 1 + assert TestStruct.Inner.fields.c == t.CStructField( + name="c", type=t.uint16_t + ) + + struct = TestStruct(a=1, b=TestStruct.Inner(c=2), d=Outer(e=3)) + assert struct.a == 1 + assert struct.b.c == 2 + assert struct.d.e == 3 + + +def test_struct_aligned_serialization_deserialization(): + class TestStruct(t.CStruct): + a: t.uint8_t + # One padding byte here + b: t.uint16_t + # No padding here + c: t.uint32_t # largest type, so the struct is 32 bit aligned + d: t.uint8_t + # Three padding bytes here + e: t.uint32_t + f: t.uint8_t + # Three more to make the struct 32 bit aligned + + assert TestStruct.get_alignment(align=False) == 1 + assert TestStruct.get_alignment(align=True) == 32 // 8 + assert TestStruct.get_size(align=False) == (1 + 2 + 4 + 1 + 4 + 1) + assert TestStruct.get_size(align=True) == ( + 1 + 2 + 4 + 1 + 4 + 1 + ) + (1 + 3 + 3) + + expected = b"" + expected += t.uint8_t(1).serialize() + expected += b"\xFF" + t.uint16_t(2).serialize() + expected += t.uint32_t(3).serialize() + expected += t.uint8_t(4).serialize() + expected += b"\xFF\xFF\xFF" + t.uint32_t(5).serialize() + expected += t.uint8_t(6).serialize() + expected += b"\xFF\xFF\xFF" + + struct = TestStruct(a=1, b=2, c=3, d=4, e=5, f=6) + assert struct.serialize(align=True) == expected + + struct2, remaining = TestStruct.deserialize(expected + b"test", align=True) + assert remaining == b"test" + assert struct == struct2 + + with pytest.raises(ValueError): + TestStruct.deserialize(expected[:-1], align=True) + + +def test_struct_aligned_nested_serialization_deserialization(): + class Inner(t.CStruct): + _padding_byte = b"\xCD" + + c: t.uint8_t + d: t.uint32_t + e: t.uint8_t + + class TestStruct(t.CStruct): + _padding_byte = b"\xAB" + + a: t.uint8_t + b: Inner + f: t.uint16_t + + expected = b"" + expected += t.uint8_t(1).serialize() + + # Inner struct + expected += b"\xAB\xAB\xAB" + t.uint8_t(2).serialize() + expected += b"\xCD\xCD\xCD" + t.uint32_t(3).serialize() + expected += t.uint8_t(4).serialize() + expected += b"\xCD\xCD\xCD" # Aligned to 4 bytes + + expected += t.uint16_t(5).serialize() + expected += b"\xAB\xAB" # Also aligned to 4 bytes due to inner struct + + struct = TestStruct(a=1, b=Inner(c=2, d=3, e=4), f=5) + assert struct.serialize(align=True) == expected + + struct2, remaining = TestStruct.deserialize(expected + b"test", align=True) + assert remaining == b"test" + assert struct == struct2 + + +def test_struct_unaligned_serialization_deserialization(): + class TestStruct(t.CStruct): + a: t.uint8_t + b: t.uint16_t + c: t.uint32_t + d: t.uint8_t + e: t.uint32_t + f: t.uint8_t + + expected = b"" + expected += t.uint8_t(1).serialize() + expected += t.uint16_t(2).serialize() + expected += t.uint32_t(3).serialize() + expected += t.uint8_t(4).serialize() + expected += t.uint32_t(5).serialize() + expected += t.uint8_t(6).serialize() + + struct = TestStruct(a=1, b=2, c=3, d=4, e=5, f=6) + + assert struct.serialize(align=False) == expected + + struct2, remaining = TestStruct.deserialize( + expected + b"test", align=False + ) + assert remaining == b"test" + assert struct == struct2 + + with pytest.raises(ValueError): + TestStruct.deserialize(expected[:-1], align=False) + + +def test_struct_equality(): + class InnerStruct(t.CStruct): + c: t.EUI64 + + class TestStruct(t.CStruct): + a: t.uint8_t + b: InnerStruct + + class TestStruct2(t.CStruct): + a: t.uint8_t + b: InnerStruct + + s1 = TestStruct( + a=2, b=InnerStruct(c=t.EUI64.convert("00:00:00:00:00:00:00:00")) + ) + s2 = TestStruct( + a=2, b=InnerStruct(c=t.EUI64.convert("00:00:00:00:00:00:00:00")) + ) + s3 = TestStruct2( + a=2, b=InnerStruct(c=t.EUI64.convert("00:00:00:00:00:00:00:00")) + ) + + assert s1 == s2 + assert s1.replace(a=3) != s1 + assert s1.replace(a=3).replace(a=2) == s1 + + assert s1 != s3 + assert s1.serialize() == s3.serialize() + + assert TestStruct(s1) == s1 + assert TestStruct(a=s1.a, b=s1.b) == s1 + + with pytest.raises(ValueError): + TestStruct(s1, b=InnerStruct(s1.b)) + + with pytest.raises(ValueError): + TestStruct2(s1) + + +def test_struct_repr(): + class TestStruct(t.CStruct): + a: t.uint8_t + b: t.uint32_t + + assert str(TestStruct(a=1, b=2)) == "TestStruct(a=1, b=2)" + assert str([TestStruct(a=1, b=2)]) == "[TestStruct(a=1, b=2)]" + + +def test_struct_bad_fields(): + with pytest.raises(TypeError): + + class TestStruct(t.CStruct): + a: t.uint8_t + b: int + + +def test_struct_incomplete_serialization(): + class TestStruct(t.CStruct): + a: t.uint8_t + b: t.uint8_t + + TestStruct(a=1, b=2).serialize() + + with pytest.raises(ValueError): + TestStruct(a=1, b=None).serialize() + + with pytest.raises(ValueError): + TestStruct(a=1).serialize() + + struct = TestStruct(a=1, b=2) + struct.b = object() + + with pytest.raises(ValueError): + struct.serialize() From ca3082fafb2c161f91d551b26bfb8b9695911736 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Mon, 13 May 2024 11:46:20 +0400 Subject: [PATCH 09/57] tests for named types --- tests/test_types_named.py | 35 +++++++++++++++++++++++++++++++++++ zigpy_zboss/types/named.py | 5 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/test_types_named.py diff --git a/tests/test_types_named.py b/tests/test_types_named.py new file mode 100644 index 0000000..443892e --- /dev/null +++ b/tests/test_types_named.py @@ -0,0 +1,35 @@ +import pytest + +import zigpy_zboss.types as t + + +def test_channel_entry(): + """Test ChannelEntry class for proper serialization, + deserialization, equality, and representation.""" + # Sample data for testing + page_data = b"\x01" # Page number as bytes + channel_mask_data = b"\x00\x01\x00\x00" # Sample channel mask as bytes + + data = page_data + channel_mask_data + + # Test deserialization + channel_entry, remaining_data = t.ChannelEntry.deserialize(data) + assert remaining_data == b'' # no extra data should remain + assert channel_entry.page == 1 + assert channel_entry.channel_mask == 0x0100 + + # Test serialization + assert channel_entry.serialize() == data + + # Test equality + another_entry = t.ChannelEntry(page=1, channel_mask=0x0100) + assert channel_entry == another_entry + assert channel_entry != t.ChannelEntry(page=0, channel_mask=0x0200) + + # Test __repr__ + expected_repr = "ChannelEntry(page=1, channels=)" + assert repr(channel_entry) == expected_repr + + # Test handling of None types for page or channel_mask + with pytest.raises(AttributeError): + t.ChannelEntry(page=None, channel_mask=None) diff --git a/zigpy_zboss/types/named.py b/zigpy_zboss/types/named.py index 67da9b3..4c43ccc 100644 --- a/zigpy_zboss/types/named.py +++ b/zigpy_zboss/types/named.py @@ -43,8 +43,11 @@ def __new__(cls, page=None, channel_mask=None): """Create a channel entry instance.""" instance = super().__new__(cls) + if page is None or channel_mask is None: + raise AttributeError("Page and channel_mask cannot be None") + instance.page = basic.uint8_t(page) - instance.channel_mask = channel_mask + instance.channel_mask = Channels(channel_mask) return instance From 039e219cf1eb99a21cfad086be6854aeb1468fcf Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Tue, 14 May 2024 15:11:45 +0400 Subject: [PATCH 10/57] tests for nvram --- tests/test_nvids.py | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/test_nvids.py diff --git a/tests/test_nvids.py b/tests/test_nvids.py new file mode 100644 index 0000000..498d6d8 --- /dev/null +++ b/tests/test_nvids.py @@ -0,0 +1,73 @@ +import zigpy_zboss.types as t +from zigpy_zboss.types import nvids +from zigpy_zboss.types.nvids import ApsSecureEntry, DSApsSecureKeys +from struct import pack + + +def test_nv_ram_get_byte_size(): + """Test the get_byte_size method of the NVRAMStruct class.""" + class TestStruct(nvids.NVRAMStruct): + a: t.uint8_t + b: t.EUI64 + c: t.uint8_t + + data = TestStruct(a=1, b=[2], c=3) + + byte_size = data.get_byte_size() + + assert byte_size == 10, f"Expected byte size to be 10, but got {byte_size}" + + +def test_dsapssecurekeys(): + """Test the serialize/deserialize method of the DSApsSecureKeys class.""" + ieee_addr1 = t.EUI64([0, 1, 2, 3, 4, 5, 6, 7]) + key1 = t.KeyData([0x10] * 16) + unknown_1_1 = t.basic.uint32_t(12345678) + entry1 = ApsSecureEntry( + ieee_addr=ieee_addr1, key=key1, _unknown_1=unknown_1_1 + ) + entry_data1 = entry1.serialize() + + ieee_addr2 = t.EUI64([8, 9, 10, 11, 12, 13, 14, 15]) + key2 = t.KeyData([0x20] * 16) + unknown_1_2 = t.basic.uint32_t(87654321) + entry2 = ApsSecureEntry( + ieee_addr=ieee_addr2, key=key2, _unknown_1=unknown_1_2 + ) + entry_data2 = entry2.serialize() + + # Calculate total length for the LVList + entry_size = ApsSecureEntry.get_byte_size() + total_length = (entry_size * 2) + 4 + + length_bytes = pack(" Date: Wed, 15 May 2024 10:52:15 +0400 Subject: [PATCH 11/57] tests for DSNwkAddrMap --- tests/test_nvids.py | 64 +++++++++++++++++++++++++++++++++++++- zigpy_zboss/types/nvids.py | 2 +- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/test_nvids.py b/tests/test_nvids.py index 498d6d8..89030eb 100644 --- a/tests/test_nvids.py +++ b/tests/test_nvids.py @@ -1,6 +1,9 @@ import zigpy_zboss.types as t from zigpy_zboss.types import nvids -from zigpy_zboss.types.nvids import ApsSecureEntry, DSApsSecureKeys +from zigpy_zboss.types.nvids import ( + ApsSecureEntry, DSApsSecureKeys, + NwkAddrMapHeader, NwkAddrMapRecord, DSNwkAddrMap +) from struct import pack @@ -71,3 +74,62 @@ def test_dsapssecurekeys(): length_bytes = pack(" bytes: header = self._header( byte_count=byte_count, entry_count=len(self), - version=t.uint8_t(0x02), + version=self.version, _align=t.uint16_t(0x0000), ) return header.serialize() + serialized_items From 9a44f5bb4e4899adfc5e949e49fe9e9f0ec2cd86 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 15 May 2024 15:02:28 +0400 Subject: [PATCH 12/57] adding commands tests --- tests/test_commands.py | 211 +++++++++++++++++++++++++++++++++++++++++ tests/test_nvids.py | 1 - 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 tests/test_commands.py diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..4bdddcc --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,211 @@ +import keyword + +from collections import defaultdict + +import pytest + +import zigpy_zboss.commands as c +from zigpy_zboss import types as t + + +def _validate_schema(schema): + for index, param in enumerate(schema): + assert isinstance(param.name, str) + assert param.name.isidentifier() + assert not keyword.iskeyword(param.name) + assert isinstance(param.type, type) + assert isinstance(param.description, str) + + # All optional params must be together at the end + if param.optional: + assert all(p.optional for p in schema[index:]) + + +def test_commands_schema(): + commands_by_id = defaultdict(list) + + for commands in c.ALL_COMMANDS: + for cmd in commands: + if cmd.definition.control_type == t.ControlType.REQ: + assert cmd.type == cmd.Req.header.control_type + assert cmd.Rsp.header.control_type == t.ControlType.RSP + + assert isinstance(cmd.Req.header, t.HLCommonHeader) + assert isinstance(cmd.Rsp.header, t.HLCommonHeader) + + assert cmd.Req.Rsp is cmd.Rsp + assert cmd.Rsp.Req is cmd.Req + assert cmd.Ind is None + + _validate_schema(cmd.Req.schema) + _validate_schema(cmd.Rsp.schema) + + commands_by_id[cmd.Req.header].append(cmd.Req) + commands_by_id[cmd.Rsp.header].append(cmd.Rsp) + + elif cmd.type == t.ControlType.IND: + assert cmd.Req is None + assert cmd.Rsp is None + + assert cmd.type == cmd.Ind.header.control_type + + assert cmd.Ind.header.control_type == t.ControlType.IND + + assert isinstance(cmd.Ind.header, t.HLCommonHeader) + + _validate_schema(cmd.Ind.schema) + + commands_by_id[cmd.Ind.header].append(cmd.Ind) + else: + assert False, "Command has unknown type" # noqa: B011 + + duplicate_commands = { + cmd: commands for cmd, + commands in commands_by_id.items() if len(commands) > 1 + } + assert not duplicate_commands + + assert len(commands_by_id.keys()) == len(c.COMMANDS_BY_ID.keys()) + + +def test_command_param_binding(): + # Example for GetModuleVersion which only requires TSN + c.NcpConfig.GetModuleVersion.Req(TSN=1) + + # Example for invalid param name + with pytest.raises(KeyError): + c.NcpConfig.GetModuleVersion.Rsp(asd=123) + + # Example for valid param name but incorrect value (invalid type) + with pytest.raises(ValueError): + c.NcpConfig.GetModuleVersion.Rsp(TSN="invalid", + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) + + # Example for correct command invocation + valid_rsp = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678) + assert isinstance(valid_rsp.FWVersion, t.uint32_t) + assert isinstance(valid_rsp.StackVersion, t.uint32_t) + assert isinstance(valid_rsp.ProtocolVersion, t.uint32_t) + + # Example for checking overflow in integer type + with pytest.raises(ValueError): + c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, FWVersion=10 ** 20, + StackVersion=789012, + ProtocolVersion=345678) + + # Invalid type in a parameter that expects a specific enum or struct + with pytest.raises(ValueError): + c.NcpConfig.SetZigbeeRole.Req(TSN=10, + DeviceRole="invalid type") + + # Coerced numerical type for a command expecting specific struct or uint + a = c.NcpConfig.SetZigbeeRole.Req(TSN=10, + DeviceRole=t.DeviceRole.ZR) + b = c.NcpConfig.SetZigbeeRole.Req(TSN=10, + DeviceRole=t.DeviceRole(1)) + + assert a == b + assert a.DeviceRole == b.DeviceRole + + assert ( + type(a.DeviceRole) == # noqa: E721 + type(b.DeviceRole) == t.DeviceRole # noqa: E721 + ) + + # Parameters can be looked up by name + zigbee_role = c.NcpConfig.GetZigbeeRole.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole.ZC) + assert zigbee_role.DeviceRole == t.DeviceRole.ZC + + # Invalid ones cannot + with pytest.raises(AttributeError): + print(zigbee_role.Oops) + + +def test_command_optional_params(): + # Basic response with required parameters only + basic_ieee_addr_rsp = c.ZDO.IeeeAddrReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), + RemoteDevNWK=t.NWK(0x1234) + ) + + # Full response including optional parameters + full_ieee_addr_rsp = c.ZDO.IeeeAddrReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), + RemoteDevNWK=t.NWK(0x1234), + NumAssocDev=5, + StartIndex=0, + AssocDevNWKList=[t.NWK(0x0001), t.NWK(0x0002)] + ) + + basic_data = basic_ieee_addr_rsp.to_frame().hl_packet.data + full_data = full_ieee_addr_rsp.to_frame().hl_packet.data + + # Check if full data contains optional parameters + assert len(full_data) >= len(basic_data) + + # Basic data should be a prefix of full data + assert full_data.startswith(basic_data) + + # Deserialization checks + IeeeAddrReq = c.ZDO.IeeeAddrReq.Rsp + assert ( + IeeeAddrReq.from_frame(basic_ieee_addr_rsp.to_frame()) + == basic_ieee_addr_rsp + ) + assert ( + IeeeAddrReq.from_frame(full_ieee_addr_rsp.to_frame()) + == full_ieee_addr_rsp + ) + + +def test_command_optional_params_failures(): + with pytest.raises(KeyError): + # Optional params cannot be skipped over + c.ZDO.IeeeAddrReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), + RemoteDevNWK=t.NWK(0x1234), + NumAssocDev=5, + # StartIndex=0, + AssocDevNWKList=[t.NWK(0x0001), t.NWK(0x0002)] + ) + + # Unless it's a partial command + partial = c.ZDO.IeeeAddrReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), + RemoteDevNWK=t.NWK(0x1234), + NumAssocDev=5, + # StartIndex=0, + AssocDevNWKList=[t.NWK(0x0001), t.NWK(0x0002)], + partial=True + ) + + # In which case, it cannot be serialized + with pytest.raises(ValueError): + partial.to_frame() diff --git a/tests/test_nvids.py b/tests/test_nvids.py index 89030eb..1c09b2b 100644 --- a/tests/test_nvids.py +++ b/tests/test_nvids.py @@ -116,7 +116,6 @@ def test_dsnwkaddrmap(): assert remaining_data == b'' assert len(result) == 2 - assert result[0].ieee_addr == dummy_map_record1.ieee_addr assert result[0].nwk_addr == dummy_map_record1.nwk_addr assert result[0].index == dummy_map_record1.index From a1c1b3a8e0a05d9a1d4def365d7d55ab3bf601cf Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 16 May 2024 13:10:43 +0400 Subject: [PATCH 13/57] more commands tests --- tests/test_commands.py | 256 ++++++++++++++++++++++++++++++++++ zigpy_zboss/types/commands.py | 5 + 2 files changed, 261 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index 4bdddcc..9d2fbc3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,4 +1,5 @@ import keyword +import dataclasses from collections import defaultdict @@ -209,3 +210,258 @@ def test_command_optional_params_failures(): # In which case, it cannot be serialized with pytest.raises(ValueError): partial.to_frame() + + +def test_simple_descriptor(): + lvlist16_type = t.LVList[t.uint16_t] + + simple_descriptor = t.SimpleDescriptor() + simple_descriptor.endpoint = t.uint8_t(1) + simple_descriptor.profile = t.uint16_t(260) + simple_descriptor.device_type = t.uint16_t(257) + simple_descriptor.device_version = t.uint8_t(0) + simple_descriptor.input_clusters = lvlist16_type( + [0, 3, 4, 5, 6, 8, 2821, 1794] + ) + simple_descriptor.output_clusters_count = t.uint8_t(2) + simple_descriptor.input_clusters_count = t.uint8_t(8) + simple_descriptor.output_clusters = lvlist16_type([0x0001, 0x0002]) + + c1 = c.ZDO.SimpleDescriptorReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + SimpleDesc=simple_descriptor, + NwkAddr=t.NWK(0x1234) + ) + + sp_simple_descriptor = t.SimpleDescriptor() + sp_simple_descriptor.endpoint = t.uint8_t(1) + sp_simple_descriptor.profile = t.uint16_t(260) + sp_simple_descriptor.device_type = t.uint16_t(257) + sp_simple_descriptor.device_version = t.uint8_t(0) + sp_simple_descriptor.input_clusters = lvlist16_type( + [0, 3, 4, 5, 6, 8, 2821, 1794] + ) + sp_simple_descriptor.output_clusters_count = t.uint8_t(2) + sp_simple_descriptor.input_clusters_count = t.uint8_t(8) + sp_simple_descriptor.output_clusters = lvlist16_type([0x0001, 0x0002]) + + c2 = c.ZDO.SimpleDescriptorReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + SimpleDesc=sp_simple_descriptor, + NwkAddr=t.NWK(0x1234) + ) + + assert c1.to_frame() == c2.to_frame() + # assert c1 == c2 + + +def test_command_str_repr(): + """Test __str__ and __repr__ methods for commands.""" + command = c.NcpConfig.GetModuleVersion.Req(TSN=1) + + assert repr(command) == str(command) + assert str([command]) == f"[{str(command)}]" + + +def test_command_immutability(): + """Test that commands are immutable.""" + command1 = c.ZDO.IeeeAddrReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RemoteDevNWK=t.NWK(0x1234), + NumAssocDev=5, + StartIndex=0, + partial=True + ) + + command2 = c.ZDO.IeeeAddrReq.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RemoteDevNWK=t.NWK(0x1234), + NumAssocDev=5, + StartIndex=0, + partial=True + ) + + d = {command1: True} + + assert command1 == command2 + assert command2 in d + assert {command1: True} == {command2: True} + + with pytest.raises(RuntimeError): + command1.partial = False + + with pytest.raises(RuntimeError): + command1.StatusCode = 20 + + with pytest.raises(RuntimeError): + command1.NumAssocDev = 5 + + with pytest.raises(RuntimeError): + del command1.StartIndex + + assert command1 == command2 + + +def test_command_serialization(): + """Test command serialization.""" + command = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678) + frame = command.to_frame() + + assert frame.hl_packet.data == bytes.fromhex( + "0A011440E20100140A0C004E460500" + ) + + # Partial frames cannot be serialized + with pytest.raises(ValueError): + partial1 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory( + 1), + StatusCode=20, + FWVersion=123456, + # StackVersion=789012, + ProtocolVersion=345678, + partial=True) + + partial1.to_frame() + + # Partial frames cannot be serialized, even if all params are filled out + with pytest.raises(ValueError): + partial2 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory( + 1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678, + partial=True) + partial2.to_frame() + + +def test_command_equality(): + """Test command equality.""" + command1 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678) + + command2 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678) + + command3 = c.NcpConfig.GetModuleVersion.Rsp(TSN=20, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678) + + assert command1 == command1 + assert command1.matches(command1) + assert command2 == command1 + assert command1 == command2 + + assert command1 != command3 + assert command3 != command1 + + assert command1.matches(command2) # Matching is a superset of equality + assert command2.matches(command1) + assert not command1.matches(command3) + assert not command3.matches(command1) + + assert not command1.matches( + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, StatusCat=t.StatusCategory(1), StatusCode=20, partial=True + ) + ) + assert c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + ).matches(command1) + + # parameters can be specified explicitly as None + assert c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + StackVersion=None, + partial=True + ).matches(command1) + assert c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + StackVersion=789012, + partial=True + ).matches(command1) + assert not c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + StackVersion=79000, + partial=True + ).matches(command1) + + # Different frame types do not match, even if they have the same structure + assert not c.ZDO.MgtLeave.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, ).matches( + c.ZDO.PermitJoin.Rsp(partial=True) + ) + + +def test_command_deserialization(): + """Test command deserialization.""" + command = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678) + + assert type(command).from_frame(command.to_frame()) == command + assert ( + command.to_frame() == + type(command).from_frame(command.to_frame()).to_frame() + ) + + # Deserialization fails if there is unparsed data at the end of the frame + frame = command.to_frame() + new_hl_packet = dataclasses.replace( + frame.hl_packet, data=frame.hl_packet.data + b"\x01" + ) + + # Create a new Frame instance with the updated hl_packet + bad_frame = dataclasses.replace(frame, hl_packet=new_hl_packet) + + with pytest.raises(ValueError): + type(command).from_frame(bad_frame) + + # Deserialization fails if you attempt to deserialize the wrong frame + with pytest.raises(ValueError): + c.ZDO.MgtLeave.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20).from_frame( + c.ZDO.PermitJoin.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20).to_frame() + ) diff --git a/zigpy_zboss/types/commands.py b/zigpy_zboss/types/commands.py index 613f749..c359f58 100644 --- a/zigpy_zboss/types/commands.py +++ b/zigpy_zboss/types/commands.py @@ -524,6 +524,11 @@ def from_frame(cls, frame, *, align=False) -> "CommandBase": else: # Otherwise, let the exception happen raise + if data: + raise ValueError( + f"Frame {frame} contains trailing data after parsing: {data}" + ) + return cls(**params) def matches(self, other: "CommandBase") -> bool: From f68185669253f5981b1a285367ea12159764f922 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Mon, 20 May 2024 11:49:20 +0400 Subject: [PATCH 14/57] tests for frames --- tests/test_frame.py | 114 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/test_frame.py diff --git a/tests/test_frame.py b/tests/test_frame.py new file mode 100644 index 0000000..c26d012 --- /dev/null +++ b/tests/test_frame.py @@ -0,0 +1,114 @@ +import pytest +import zigpy_zboss.types as t +from zigpy_zboss.frames import Frame, InvalidFrame, CRC8, HLPacket + + +def test_frame_deserialization(): + """Test frame deserialization.""" + ll_signature = t.uint16_t(0xADDE).serialize() + + # Create an HLCommonHeader with specific fields + hl_data = t.Bytes(b"test_data").serialize() + hl_packet = hl_data + + ll_size = t.uint16_t(len(hl_packet) + 5).serialize() + ll_type = t.uint8_t(0x01).serialize() + ll_flags = t.LLFlags(0x00).serialize() + + ll_header_without_crc = ll_signature + ll_size + ll_type + ll_flags + ll_crc = CRC8(ll_header_without_crc[2:6]).digest().serialize() + ll_header = ll_header_without_crc + ll_crc + + frame_data = ll_header + hl_packet + extra_data = b"extra_data" + + # Deserialize frame + frame, rest = Frame.deserialize(frame_data + extra_data) + + # Assertions + assert rest == extra_data + assert frame.ll_header.signature == 0xADDE + assert frame.ll_header.size == len(hl_packet) + 5 + assert frame.ll_header.frame_type == 0x01 + assert frame.ll_header.flags == 0x00 + assert frame.ll_header.crc8 == CRC8(ll_header_without_crc[2:6]).digest() + assert frame.hl_packet.data == b"test_data" + + # Invalid frame signature + invalid_signature_frame_data = t.uint16_t(0xFFFF).serialize() + frame_data[ + 2:] + with pytest.raises(InvalidFrame, + match="Expected frame to start with Signature"): + Frame.deserialize(invalid_signature_frame_data) + + # Invalid CRC8 + ll_header = ll_header_without_crc + + frame_data_without_crc = ll_header + hl_packet + with pytest.raises(InvalidFrame, match="Invalid frame checksum"): + Frame.deserialize(frame_data_without_crc) + + +def test_ack_flag_deserialization(): + """Test frame deserialization with ACK flag.""" + ll_signature = t.uint16_t(0xADDE).serialize() + ll_size = t.uint16_t(5).serialize() # Only LLHeader size + ll_type = t.uint8_t(0x01).serialize() + ll_flags = t.LLFlags(t.LLFlags.isACK).serialize() + + ll_header_without_crc = ll_signature + ll_size + ll_type + ll_flags + ll_crc = CRC8(ll_header_without_crc[2:6]).digest().serialize() + ll_header = ll_header_without_crc + ll_crc + + frame_data = ll_header + extra_data = b"extra_data" + + frame, rest = Frame.deserialize(frame_data + extra_data) + + assert rest == extra_data + assert frame.ll_header.signature == 0xADDE + assert frame.ll_header.size == 5 + assert frame.ll_header.frame_type == 0x01 + assert frame.ll_header.flags == t.LLFlags.isACK + assert frame.ll_header.crc8 == CRC8(ll_header_without_crc[2:6]).digest() + assert frame.hl_packet is None + + +def test_first_frag_flag_deserialization(): + """Test frame deserialization with FirstFrag flag.""" + ll_signature = t.uint16_t(0xADDE).serialize() + + # Create an HLCommonHeader with specific fields + hl_header = t.HLCommonHeader( + version=0x01, type=t.ControlType.RSP, id=0x1234 + ) + hl_data = t.Bytes(b"test_data") + + # Create HLPacket and serialize + hl_packet = HLPacket(header=hl_header, data=hl_data) + hl_packet_data = hl_packet.serialize() + + # Create LLHeader with FirstFrag flag + ll_size = t.uint16_t(len(hl_packet_data) + 5).serialize() + ll_type = t.uint8_t(0x01).serialize() + ll_flags = t.LLFlags(t.LLFlags.FirstFrag).serialize() + + ll_header_without_crc = ll_signature + ll_size + ll_type + ll_flags + ll_crc = CRC8(ll_header_without_crc[2:6]).digest().serialize() + ll_header = ll_header_without_crc + ll_crc + + frame_data = ll_header + hl_packet_data + extra_data = b"extra_data" + + frame, rest = Frame.deserialize(frame_data + extra_data) + + assert rest == extra_data + assert frame.ll_header.signature == 0xADDE + assert frame.ll_header.size == len(hl_packet_data) + 5 + assert frame.ll_header.frame_type == 0x01 + assert frame.ll_header.flags == t.LLFlags.FirstFrag + assert frame.ll_header.crc8 == CRC8(ll_header_without_crc[2:6]).digest() + assert frame.hl_packet.header.version == 0x01 + assert frame.hl_packet.header.control_type == t.ControlType.RSP + assert frame.hl_packet.header.id == 0x1234 + assert frame.hl_packet.data == b"test_data" From 16ff8ccd6e0cf3909a74a032a27dce30786c9796 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Tue, 21 May 2024 11:28:47 +0400 Subject: [PATCH 15/57] tests for frame fragmentation --- tests/test_frame.py | 129 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/tests/test_frame.py b/tests/test_frame.py index c26d012..2fdd300 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -1,6 +1,9 @@ import pytest import zigpy_zboss.types as t -from zigpy_zboss.frames import Frame, InvalidFrame, CRC8, HLPacket +from zigpy_zboss.frames import ( + Frame, InvalidFrame, CRC8, + HLPacket, ZBNCP_LL_BODY_SIZE_MAX, LLHeader +) def test_frame_deserialization(): @@ -112,3 +115,127 @@ def test_first_frag_flag_deserialization(): assert frame.hl_packet.header.control_type == t.ControlType.RSP assert frame.hl_packet.header.id == 0x1234 assert frame.hl_packet.data == b"test_data" + + +def test_handle_tx_fragmentation(): + """Test the handle_tx_fragmentation method for proper fragmentation.""" + # Create an HLCommonHeader with specific fields + hl_header = t.HLCommonHeader( + version=0x01, type=t.ControlType.RSP, id=0x1234 + ) + large_data = b"a" * (ZBNCP_LL_BODY_SIZE_MAX * 2 + 50) + hl_data = t.Bytes(large_data) + + # Create an HLPacket with the large data + hl_packet = HLPacket(header=hl_header, data=hl_data) + frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) + + fragments = frame.handle_tx_fragmentation() + + total_fragments = frame.count_fragments() + assert len(fragments) == total_fragments + + # Calculate the expected size of the first fragment + # Exclude the CRC16 for size calculation + serialized_hl_packet = hl_packet.serialize()[2:] + first_frag_size = ( + len(serialized_hl_packet) % ZBNCP_LL_BODY_SIZE_MAX + or ZBNCP_LL_BODY_SIZE_MAX + ) + + # Check the first fragment + first_fragment = fragments[0] + assert first_fragment.ll_header.flags == t.LLFlags.FirstFrag + assert first_fragment.ll_header.size == first_frag_size + 7 + assert len(first_fragment.hl_packet.data) == first_frag_size - 4 + + # Check the middle fragments + for middle_fragment in fragments[1:-1]: + assert middle_fragment.ll_header.flags == 0 + assert middle_fragment.ll_header.size == ZBNCP_LL_BODY_SIZE_MAX + 7 + assert len(middle_fragment.hl_packet.data) == ZBNCP_LL_BODY_SIZE_MAX + + # Check the last fragment + last_fragment = fragments[-1] + last_frag_size = ( + len(serialized_hl_packet) - + (first_frag_size + (total_fragments - 2) * ZBNCP_LL_BODY_SIZE_MAX) + ) + assert last_fragment.ll_header.flags == t.LLFlags.LastFrag + assert last_fragment.ll_header.size == last_frag_size + 7 + assert len(last_fragment.hl_packet.data) == last_frag_size + + +def test_handle_tx_fragmentation_edge_cases(): + """Test the handle_tx_fragmentation method for various edge cases.""" + # Data size exactly equal to ZBNCP_LL_BODY_SIZE_MAX + exact_size_data = b"a" * (ZBNCP_LL_BODY_SIZE_MAX - 2 - 2) + hl_header = t.HLCommonHeader(version=0x01, type=t.ControlType.RSP, + id=0x1234) + hl_packet = HLPacket(header=hl_header, data=t.Bytes(exact_size_data)) + frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) + + # Perform fragmentation + fragments = frame.handle_tx_fragmentation() + assert len(fragments) == 1 # Should not fragment + + # Test with data size just above ZBNCP_LL_BODY_SIZE_MAX + just_above_size_data = b"a" * (ZBNCP_LL_BODY_SIZE_MAX + 1 - 2 - 2) + hl_packet = HLPacket(header=hl_header, data=t.Bytes(just_above_size_data)) + frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) + fragments = frame.handle_tx_fragmentation() + assert len(fragments) == 2 # Should fragment into two + + # Test with data size much larger than ZBNCP_LL_BODY_SIZE_MAX + large_data = b"a" * ((ZBNCP_LL_BODY_SIZE_MAX * 5) + 50 - 2 - 2) + hl_packet = HLPacket(header=hl_header, data=t.Bytes(large_data)) + frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) + fragments = frame.handle_tx_fragmentation() + assert len(fragments) == 6 # 5 full fragments and 1 partial fragment + + # Test with very small data + small_data = b"a" * 10 + hl_packet = HLPacket(header=hl_header, data=t.Bytes(small_data)) + frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) + fragments = frame.handle_tx_fragmentation() + assert len(fragments) == 1 # Should not fragment + + +def test_handle_rx_fragmentation(): + """Test the handle_rx_fragmentation method for. + + proper reassembly of fragments. + """ + # Create an HLCommonHeader with specific fields + hl_header = t.HLCommonHeader( + version=0x01, type=t.ControlType.RSP, id=0x1234 + ) + large_data = b"a" * (ZBNCP_LL_BODY_SIZE_MAX * 2 + 50) + hl_data = t.Bytes(large_data) + + # Create an HLPacket with the large data + hl_packet = HLPacket(header=hl_header, data=hl_data) + frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) + + # Perform fragmentation + fragments = frame.handle_tx_fragmentation() + + # Verify that the correct number of fragments was created + total_fragments = frame.count_fragments() + assert len(fragments) == total_fragments + + # Reassemble the fragments using handle_rx_fragmentation + reassembled_frame = Frame.handle_rx_fragmentation(fragments) + + # Verify the reassembled frame + assert ( + reassembled_frame.ll_header.frame_type == t.TYPE_ZBOSS_NCP_API_HL + ) + assert ( + reassembled_frame.ll_header.flags == + (t.LLFlags.FirstFrag | t.LLFlags.LastFrag) + ) + + # Verify the reassembled data matches the original data + reassembled_data = reassembled_frame.hl_packet.data + assert reassembled_data == large_data From 86aa8f8c9c5d96092b21687cd2891dc518ca0027 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 22 May 2024 13:11:02 +0400 Subject: [PATCH 16/57] tests for api connections, add some fixtures. --- tests/api/__init__.py | 1 + tests/api/test_connect.py | 68 +++++++++ tests/conftest.py | 307 ++++++++++++++++++++++++++++++++++++++ tests/test_uart.py | 6 +- zigpy_zboss/api.py | 9 +- 5 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 tests/api/__init__.py create mode 100644 tests/api/test_connect.py create mode 100644 tests/conftest.py diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..9ef26fa --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1 @@ +"""Tests for api.""" \ No newline at end of file diff --git a/tests/api/test_connect.py b/tests/api/test_connect.py new file mode 100644 index 0000000..f3bc685 --- /dev/null +++ b/tests/api/test_connect.py @@ -0,0 +1,68 @@ +"""Test cases for zigpy-zboss API connect/close methods.""" +import pytest +from ..conftest import config_for_port_path, BaseServerZBOSS + +from zigpy_zboss.api import ZBOSS + + +@pytest.mark.asyncio +async def test_connect_no_test(make_zboss_server): + """Test that ZBOSS.connect() can connect.""" + zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) + zboss = ZBOSS(config_for_port_path(zboss_server.port_path)) + + await zboss.connect() + + # Nothing will be sent + assert zboss_server._uart.data_received.call_count == 0 + + zboss.close() + + +@pytest.mark.asyncio +async def test_api_close(connected_zboss, mocker): + """Test that ZBOSS.close() properly cleans up the object.""" + zboss, zboss_server = connected_zboss + uart = zboss._uart + mocker.spy(uart, "close") + + # add some dummy fields and listeners, should be cleared on close + zboss.version = 2 + zboss.capabilities = 4 + zboss._listeners = { + 'listener1': [mocker.Mock()], 'listener2': [mocker.Mock()] + } + + zboss.close() + + # Make sure our UART was actually closed + assert zboss._uart is None + assert zboss._app is None + assert uart.close.call_count == 2 + + # ZBOSS.close should not throw any errors if called multiple times + zboss.close() + zboss.close() + + def dict_minus(d, minus): + return {k: v for k, v in d.items() if k not in minus} + + ignored_keys = ["_blocking_request_lock", "nvram"] + + # Closing ZBOSS should reset it completely to that of a fresh object + # We have to ignore our mocked method and the lock + zboss2 = ZBOSS(zboss._config) + assert ( + zboss2._blocking_request_lock.locked() + == zboss._blocking_request_lock.locked() + ) + assert dict_minus(zboss.__dict__, ignored_keys) == dict_minus( + zboss2.__dict__, ignored_keys + ) + + zboss2.close() + zboss2.close() + + assert dict_minus(zboss.__dict__, ignored_keys) == dict_minus( + zboss2.__dict__, ignored_keys + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a5494d4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,307 @@ +"""Shared fixtures and utilities for testing zigpy-zboss.""" +import asyncio +import sys +import inspect +import pytest +import typing +import gc +import logging + + +from unittest.mock import Mock, PropertyMock, patch + +import zigpy_zboss.config as conf +from zigpy_zboss.uart import ZbossNcpProtocol +import zigpy_zboss.types as t +from zigpy_zboss.api import ZBOSS + +LOGGER = logging.getLogger(__name__) + +FAKE_SERIAL_PORT = "/dev/ttyFAKE0" + + +# Globally handle async tests and error on unawaited coroutines +def pytest_collection_modifyitems(session, config, items): + for item in items: + item.add_marker( + pytest.mark.filterwarnings( + "error::pytest.PytestUnraisableExceptionWarning" + ) + ) + item.add_marker(pytest.mark.filterwarnings("error::RuntimeWarning")) + + +@pytest.hookimpl(trylast=True) +def pytest_fixture_post_finalizer(fixturedef, request) -> None: + """Called after fixture teardown""" + if fixturedef.argname != "event_loop": + return + + policy = asyncio.get_event_loop_policy() + try: + loop = policy.get_event_loop() + except RuntimeError: + loop = None + if loop is not None: + # Cleanup code based on the implementation of asyncio.run() + try: + if not loop.is_closed(): + asyncio.runners._cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + if sys.version_info >= (3, 9): + loop.run_until_complete(loop.shutdown_default_executor()) + finally: + loop.close() + new_loop = policy.new_event_loop() # Replace existing event loop + # Ensure subsequent calls to get_event_loop() succeed + policy.set_event_loop(new_loop) + + +@pytest.fixture +def event_loop( + request: pytest.FixtureRequest, +) -> typing.Iterator[asyncio.AbstractEventLoop]: + """Create an instance of the default event loop for each test case.""" + yield asyncio.get_event_loop_policy().new_event_loop() + # Call the garbage collector to trigger ResourceWarning's as soon + # as possible (these are triggered in various __del__ methods). + # Without this, resources opened in one test can fail other tests + # when the warning is generated. + gc.collect() + # Event loop cleanup handled by pytest_fixture_post_finalizer + + +class ForwardingSerialTransport: + """ + Serial transport that hooks directly into a protocol + """ + + def __init__(self, protocol): + self.protocol = protocol + self._is_connected = False + self.other = None + + self.serial = Mock() + self.serial.name = FAKE_SERIAL_PORT + self.serial.baudrate = 45678 + type(self.serial).dtr = self._mock_dtr_prop = PropertyMock( + return_value=None + ) + type(self.serial).rts = self._mock_rts_prop = PropertyMock( + return_value=None + ) + + def _connect(self): + assert not self._is_connected + self._is_connected = True + self.other.protocol.connection_made(self) + + def write(self, data): + assert self._is_connected + self.protocol.data_received(data) + + def close( + self, *, error=ValueError("Connection was closed") # noqa: B008 + ): + LOGGER.debug("Closing %s", self) + + if not self._is_connected: + return + + self._is_connected = False + + # Our own protocol gets gracefully closed + self.other.close(error=None) + + # The protocol we're forwarding to gets the error + self.protocol.connection_lost(error) + + def __repr__(self): + return f"<{type(self).__name__} to {self.protocol}>" + + +def config_for_port_path(path): + return conf.CONFIG_SCHEMA( + { + conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: path}, + conf.CONF_DEVICE_BAUDRATE: 115200, + conf.CONF_DEVICE_FLOW_CONTROL: None + } + ) + + +@pytest.fixture +def make_zboss_server(mocker): + transports = [] + + def inner(server_cls, config=None, shorten_delays=True): + if config is None: + config = config_for_port_path(FAKE_SERIAL_PORT) + + if shorten_delays: + mocker.patch( + "zigpy_zboss.api.AFTER_BOOTLOADER_SKIP_BYTE_DELAY", 0.001 + ) + mocker.patch("zigpy_zboss.api.BOOTLOADER_PIN_TOGGLE_DELAY", 0.001) + + server = server_cls(config) + server._transports = transports + + server.port_path = FAKE_SERIAL_PORT + server._uart = None + + def passthrough_serial_conn( + loop, protocol_factory, url, *args, **kwargs + ): + LOGGER.info("Intercepting serial connection to %s", url) + + assert url == FAKE_SERIAL_PORT + + # No double connections! + if any([t._is_connected for t in transports]): + raise RuntimeError( + "Cannot open two connections to the same serial port" + ) + if server._uart is None: + server._uart = ZbossNcpProtocol( + config[conf.CONF_DEVICE], server + ) + mocker.spy(server._uart, "data_received") + + client_protocol = protocol_factory() + + # Client writes go to the server + client_transport = ForwardingSerialTransport(server._uart) + transports.append(client_transport) + + # Server writes go to the client + server_transport = ForwardingSerialTransport(client_protocol) + + # Notify them of one another + server_transport.other = client_transport + client_transport.other = server_transport + + # And finally connect both simultaneously + server_transport._connect() + client_transport._connect() + + fut = loop.create_future() + fut.set_result((client_transport, client_protocol)) + + return fut + + mocker.patch( + "serial_asyncio.create_serial_connection", + new=passthrough_serial_conn + ) + + # So we don't have to import it every time + server.serial_port = FAKE_SERIAL_PORT + + return server + + yield inner + + +@pytest.fixture +def make_connected_zboss(make_zboss_server, mocker): + async def inner(server_cls): + config = conf.CONFIG_SCHEMA( + { + conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: FAKE_SERIAL_PORT}, + } + ) + + zboss = ZBOSS(config) + zboss_server = make_zboss_server(server_cls=server_cls) + + await zboss.connect() + + zboss.nvram.align_structs = server_cls.align_structs + zboss.version = server_cls.version + + return zboss, zboss_server + + return inner + + +@pytest.fixture +def connected_zboss(event_loop, make_connected_zboss): + zboss, zboss_server = event_loop.run_until_complete( + make_connected_zboss(BaseServerZBOSS) + ) + yield zboss, zboss_server + zboss.close() + + +class BaseServerZBOSS(ZBOSS): + align_structs = False + version = None + + async def _flatten_responses(self, request, responses): + if responses is None: + return + elif isinstance(responses, t.CommandBase): + yield responses + elif inspect.iscoroutinefunction(responses): + async for rsp in responses(request): + yield rsp + elif inspect.isasyncgen(responses): + async for rsp in responses: + yield rsp + elif callable(responses): + async for rsp in self._flatten_responses( + request, responses(request) + ): + yield rsp + else: + for response in responses: + async for rsp in self._flatten_responses(request, response): + yield rsp + + async def _send_responses(self, request, responses): + async for response in self._flatten_responses(request, responses): + await asyncio.sleep(0.001) + LOGGER.debug( + "Replying to %s with %s", request, response + ) + self.send(response) + + def reply_once_to(self, request, responses, *, override=False): + if override: + self._listeners[request.header].clear() + + request_future = self.wait_for_response(request) + + async def replier(): + request = await request_future + await self._send_responses(request, responses) + + return request + + return asyncio.create_task(replier()) + + def reply_to(self, request, responses, *, override=False): + if override: + self._listeners[request.header].clear() + + async def callback(request): + callback.call_count += 1 + await self._send_responses(request, responses) + + callback.call_count = 0 + + self.register_indication_listener( + request, lambda r: asyncio.create_task(callback(r)) + ) + + return callback + + def send(self, response): + if response is not None and self._uart is not None: + self._uart.send(response.to_frame(align=self.align_structs)) + + def close(self): + # We don't clear listeners on shutdown + with patch.object(self, "_listeners", {}): + return super().close() diff --git a/tests/test_uart.py b/tests/test_uart.py index f2db4b7..e586d72 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -11,16 +11,16 @@ @pytest.fixture def connected_uart(mocker): - znp = mocker.Mock() + zboss = mocker.Mock() config = { conf.CONF_DEVICE_PATH: "/dev/ttyACM0", conf.CONF_DEVICE_BAUDRATE: 115200, conf.CONF_DEVICE_FLOW_CONTROL: None} - uart = znp_uart.ZbossNcpProtocol(config, znp) + uart = znp_uart.ZbossNcpProtocol(config, zboss) uart.connection_made(mocker.Mock()) - yield znp, uart + yield zboss, uart def ll_checksum(frame): diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 599b8a1..d37a21b 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -44,6 +44,7 @@ def __init__(self, config: conf.ConfigType): self._blocking_request_lock = asyncio.Lock() self.capabilities = None + self.version = None self.nvram = NVRAMHelper(self) self.network_info: zigpy.state.NetworkInformation = None @@ -110,7 +111,14 @@ def close(self) -> None: ZBOSS instance. """ self._app = None + + for _header, listeners in self._listeners.items(): + for listener in listeners: + listener.cancel() + + self._listeners.clear() self.version = None + self.capabilities = None if self._uart is not None: self._uart.close() @@ -118,7 +126,6 @@ def close(self) -> None: def frame_received(self, frame: Frame) -> bool: """Frame has been received. - Called when a frame has been received. Returns whether or not the frame was handled by any listener. From e3ecced2e2bafa08b4c8ffe36d7692fbbe67e0a8 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 22 May 2024 13:41:34 +0400 Subject: [PATCH 17/57] keep callback bindings on connection close --- tests/api/__init__.py | 2 +- tests/api/test_connect.py | 2 +- zigpy_zboss/api.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/api/__init__.py b/tests/api/__init__.py index 9ef26fa..ea431d6 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -1 +1 @@ -"""Tests for api.""" \ No newline at end of file +"""Tests for api.""" diff --git a/tests/api/test_connect.py b/tests/api/test_connect.py index f3bc685..1c7393f 100644 --- a/tests/api/test_connect.py +++ b/tests/api/test_connect.py @@ -47,7 +47,7 @@ async def test_api_close(connected_zboss, mocker): def dict_minus(d, minus): return {k: v for k, v in d.items() if k not in minus} - ignored_keys = ["_blocking_request_lock", "nvram"] + ignored_keys = ["_blocking_request_lock", "nvram", "_listeners"] # Closing ZBOSS should reset it completely to that of a fresh object # We have to ignore our mocked method and the lock diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index d37a21b..0c8e7b1 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -116,7 +116,6 @@ def close(self) -> None: for listener in listeners: listener.cancel() - self._listeners.clear() self.version = None self.capabilities = None From 9e139af3440f933e9c2e90d1b42d36ab5494d1a6 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 23 May 2024 14:57:28 +0400 Subject: [PATCH 18/57] tests for api listeners --- tests/api/test_connect.py | 2 +- tests/api/test_listeners.py | 182 ++++++++++++++++++++++++++++++++++++ zigpy_zboss/api.py | 1 + 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 tests/api/test_listeners.py diff --git a/tests/api/test_connect.py b/tests/api/test_connect.py index 1c7393f..f3bc685 100644 --- a/tests/api/test_connect.py +++ b/tests/api/test_connect.py @@ -47,7 +47,7 @@ async def test_api_close(connected_zboss, mocker): def dict_minus(d, minus): return {k: v for k, v in d.items() if k not in minus} - ignored_keys = ["_blocking_request_lock", "nvram", "_listeners"] + ignored_keys = ["_blocking_request_lock", "nvram"] # Closing ZBOSS should reset it completely to that of a fresh object # We have to ignore our mocked method and the lock diff --git a/tests/api/test_listeners.py b/tests/api/test_listeners.py new file mode 100644 index 0000000..18f1af5 --- /dev/null +++ b/tests/api/test_listeners.py @@ -0,0 +1,182 @@ +import asyncio +from unittest.mock import call + +import pytest + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c +from zigpy_zboss.api import OneShotResponseListener, IndicationListener + + +@pytest.mark.asyncio +async def test_resolve(event_loop, mocker): + callback = mocker.Mock() + callback_listener = IndicationListener( + [c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + )], callback + ) + + future = event_loop.create_future() + one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + )], future) + + match = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + no_match = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3, + ) + + assert callback_listener.resolve(match) + assert not callback_listener.resolve(no_match) + assert callback_listener.resolve(match) + assert not callback_listener.resolve(no_match) + + assert one_shot_listener.resolve(match) + assert not one_shot_listener.resolve(no_match) + + callback.assert_has_calls([call(match), call(match)]) + assert callback.call_count == 2 + + assert (await future) == match + + # Cancelling a callback will have no effect + assert not callback_listener.cancel() + + # Cancelling a one-shot listener does not throw any errors + assert one_shot_listener.cancel() + assert one_shot_listener.cancel() + assert one_shot_listener.cancel() + + +@pytest.mark.asyncio +async def test_cancel(event_loop): + # Cancelling a one-shot listener prevents it from being fired + future = event_loop.create_future() + one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )], future) + one_shot_listener.cancel() + + match = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + assert not one_shot_listener.resolve(match) + + with pytest.raises(asyncio.CancelledError): + await future + + +@pytest.mark.asyncio +async def test_multi_cancel(event_loop, mocker): + callback = mocker.Mock() + callback_listener = IndicationListener( + [c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )], callback + ) + + future = event_loop.create_future() + one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )], future) + + match = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + no_match = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3, + ) + + assert callback_listener.resolve(match) + assert not callback_listener.resolve(no_match) + + assert one_shot_listener.resolve(match) + assert not one_shot_listener.resolve(no_match) + + callback.assert_called_once_with(match) + assert (await future) == match + + +@pytest.mark.asyncio +async def test_api_cancel_listeners(connected_zboss, mocker): + zboss, zboss_server = connected_zboss + + callback = mocker.Mock() + + zboss.register_indication_listener( + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), callback + ) + future = zboss.wait_for_responses( + [ + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3, + ), + ] + ) + + assert not future.done() + zboss.close() + + with pytest.raises(asyncio.CancelledError): + await future + + # add_done_callback won't be executed immediately + await asyncio.sleep(0.1) + + # only one shot listerner is cleared + # we do not remove indication listeners + # because + assert len(zboss._listeners) == 0 diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index 0c8e7b1..f074fea 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -115,6 +115,7 @@ def close(self) -> None: for _header, listeners in self._listeners.items(): for listener in listeners: listener.cancel() + self._listeners.clear() self.version = None self.capabilities = None From 29dded57c861673523872b507450d06bab26f7d0 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 23 May 2024 18:14:13 +0400 Subject: [PATCH 19/57] tests for api requests --- tests/api/test_request.py | 187 ++++++++++++++++++++++++++++++++++++++ tests/conftest.py | 6 +- zigpy_zboss/api.py | 4 + 3 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 tests/api/test_request.py diff --git a/tests/api/test_request.py b/tests/api/test_request.py new file mode 100644 index 0000000..3424a72 --- /dev/null +++ b/tests/api/test_request.py @@ -0,0 +1,187 @@ +import asyncio +import logging + +import pytest +import async_timeout + +import zigpy_zboss.types as t +import zigpy_zboss.config as conf +import zigpy_zboss.commands as c +from zigpy_zboss.frames import ( + Frame, InvalidFrame, CRC8, + HLPacket, ZBNCP_LL_BODY_SIZE_MAX, LLHeader +) + + +@pytest.mark.asyncio +async def test_cleanup_timeout_internal(connected_zboss): + zboss, zboss_server = connected_zboss + + assert not any(zboss._listeners.values()) + + with pytest.raises(asyncio.TimeoutError): + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 0.1) + + # We should be cleaned up + assert not any(zboss._listeners.values()) + + zboss.close() + + +@pytest.mark.asyncio +async def test_cleanup_timeout_external(connected_zboss): + zboss, zboss_server = connected_zboss + + assert not any(zboss._listeners.values()) + + # This request will timeout because we didn't send anything back + with pytest.raises(asyncio.TimeoutError): + async with async_timeout.timeout(0.1): + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 10) + + # We should be cleaned up + assert not any(zboss._listeners.values()) + + zboss.close() + await asyncio.sleep(0.1) + + +@pytest.mark.asyncio +async def test_zboss_request_kwargs(connected_zboss, event_loop): + zboss, zboss_server = connected_zboss + + # Invalid format + with pytest.raises(KeyError): + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSNT=1), 10) + + # Valid format, invalid name + with pytest.raises(KeyError): + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TsN=1), 10) + + # Valid format, valid name + ping_rsp = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ) + + async def send_ping_response(): + await zboss_server.send(ping_rsp) + + event_loop.call_soon(asyncio.create_task, send_ping_response()) + + assert ( + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 2) + ) == ping_rsp + + # You cannot send anything but requests + with pytest.raises(ValueError): + await zboss.request(c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + )) + + # You cannot send indications + with pytest.raises(ValueError): + await zboss.request( + c.NWK.NwkLeaveInd.Ind(partial=True) + ) + zboss.close() + + +@pytest.mark.asyncio +async def test_zboss_sreq_srsp(connected_zboss, event_loop): + zboss, zboss_server = connected_zboss + + # Each SREQ must have a corresponding SRSP, so this will fail + with pytest.raises(asyncio.TimeoutError): + async with async_timeout.timeout(0.5): + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 10) + + # This will work + ping_rsp = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ) + + async def send_ping_response(): + await zboss_server.send(ping_rsp) + + event_loop.call_soon(asyncio.create_task, send_ping_response()) + + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 10) + + zboss.close() + +# @pytest.mark.asyncio +# async def test_zboss_unknown_frame(connected_zboss, caplog): +# zboss, _ = connected_zboss +# hl_header = t.HLCommonHeader( +# version=0x0121, type=0xFFFF, id=0x123421 +# ) +# hl_packet = HLPacket(header=hl_header, data=t.Bytes()) +# frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) +# +# caplog.set_level(logging.ERROR) +# zboss.frame_received(frame) +# +# # Unknown frames are logged in their entirety but an error is not thrown +# assert repr(frame) in caplog.text +# +# zboss.close() + + +# async def test_handling_known_bad_command_parsing(connected_zboss, caplog): +# zboss, _ = connected_zboss +# +# bad_frame = GeneralFrame( +# header=t.CommandHeader( +# id=0x9F, subsystem=t.Subsystem.ZDO, type=t.CommandType.AREQ +# ), +# data=b"\x13\xDB\x84\x01\x21", +# ) +# +# caplog.set_level(logging.WARNING) +# zboss.frame_received(bad_frame) +# +# # The frame is expected to fail to parse so will be +# # logged as only a warning +# assert len(caplog.records) == 1 +# assert caplog.records[0].levelname == "WARNING" +# assert repr(bad_frame) in caplog.messages[0] +# +# +# async def test_handling_unknown_bad_command_parsing(connected_zboss): +# zboss, _ = connected_zboss +# +# bad_frame = GeneralFrame( +# header=t.CommandHeader( +# id=0xCB, subsystem=t.Subsystem.ZDO, type=t.CommandType.AREQ +# ), +# data=b"\x13\xDB\x84\x01\x21", +# ) +# +# with pytest.raises(ValueError): +# zboss.frame_received(bad_frame) +# +# + +@pytest.mark.asyncio +async def test_send_failure_when_disconnected(connected_zboss): + zboss, _ = connected_zboss + zboss._uart = None + + with pytest.raises(RuntimeError) as e: + await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 10) + + assert "Coordinator is disconnected" in str(e.value) + zboss.close() diff --git a/tests/conftest.py b/tests/conftest.py index a5494d4..432e2b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -265,7 +265,7 @@ async def _send_responses(self, request, responses): LOGGER.debug( "Replying to %s with %s", request, response ) - self.send(response) + await self.send(response) def reply_once_to(self, request, responses, *, override=False): if override: @@ -297,9 +297,9 @@ async def callback(request): return callback - def send(self, response): + async def send(self, response): if response is not None and self._uart is not None: - self._uart.send(response.to_frame(align=self.align_structs)) + await self._uart.send(response.to_frame(align=self.align_structs)) def close(self): # We don't clear listeners on shutdown diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index f074fea..db18480 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -188,6 +188,10 @@ async def request( raise ValueError( f"Cannot send a command that isn't a request: {request!r}") + if self._uart is None: + raise RuntimeError( + "Coordinator is disconnected, cannot send request") + frame = request.to_frame() # If the frame is too long, it needs fragmentation. fragments = frame.handle_tx_fragmentation() From 5622abc8b5b35506ba611e90978049dfdabb3174 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 24 May 2024 12:12:36 +0400 Subject: [PATCH 20/57] tests for frame_received --- tests/api/test_request.py | 124 +++++++++++++++++++------------------- zigpy_zboss/uart.py | 5 ++ 2 files changed, 67 insertions(+), 62 deletions(-) diff --git a/tests/api/test_request.py b/tests/api/test_request.py index 3424a72..4d2c756 100644 --- a/tests/api/test_request.py +++ b/tests/api/test_request.py @@ -5,11 +5,9 @@ import async_timeout import zigpy_zboss.types as t -import zigpy_zboss.config as conf import zigpy_zboss.commands as c from zigpy_zboss.frames import ( - Frame, InvalidFrame, CRC8, - HLPacket, ZBNCP_LL_BODY_SIZE_MAX, LLHeader + Frame, HLPacket, ZBNCP_LL_BODY_SIZE_MAX, LLHeader ) @@ -25,8 +23,6 @@ async def test_cleanup_timeout_internal(connected_zboss): # We should be cleaned up assert not any(zboss._listeners.values()) - zboss.close() - @pytest.mark.asyncio async def test_cleanup_timeout_external(connected_zboss): @@ -42,9 +38,6 @@ async def test_cleanup_timeout_external(connected_zboss): # We should be cleaned up assert not any(zboss._listeners.values()) - zboss.close() - await asyncio.sleep(0.1) - @pytest.mark.asyncio async def test_zboss_request_kwargs(connected_zboss, event_loop): @@ -91,7 +84,6 @@ async def send_ping_response(): await zboss.request( c.NWK.NwkLeaveInd.Ind(partial=True) ) - zboss.close() @pytest.mark.asyncio @@ -120,60 +112,23 @@ async def send_ping_response(): await zboss.request(c.NcpConfig.GetModuleVersion.Req(TSN=1), 10) - zboss.close() -# @pytest.mark.asyncio -# async def test_zboss_unknown_frame(connected_zboss, caplog): -# zboss, _ = connected_zboss -# hl_header = t.HLCommonHeader( -# version=0x0121, type=0xFFFF, id=0x123421 -# ) -# hl_packet = HLPacket(header=hl_header, data=t.Bytes()) -# frame = Frame(ll_header=LLHeader(), hl_packet=hl_packet) -# -# caplog.set_level(logging.ERROR) -# zboss.frame_received(frame) -# -# # Unknown frames are logged in their entirety but an error is not thrown -# assert repr(frame) in caplog.text -# -# zboss.close() - - -# async def test_handling_known_bad_command_parsing(connected_zboss, caplog): -# zboss, _ = connected_zboss -# -# bad_frame = GeneralFrame( -# header=t.CommandHeader( -# id=0x9F, subsystem=t.Subsystem.ZDO, type=t.CommandType.AREQ -# ), -# data=b"\x13\xDB\x84\x01\x21", -# ) -# -# caplog.set_level(logging.WARNING) -# zboss.frame_received(bad_frame) -# -# # The frame is expected to fail to parse so will be -# # logged as only a warning -# assert len(caplog.records) == 1 -# assert caplog.records[0].levelname == "WARNING" -# assert repr(bad_frame) in caplog.messages[0] -# -# -# async def test_handling_unknown_bad_command_parsing(connected_zboss): -# zboss, _ = connected_zboss -# -# bad_frame = GeneralFrame( -# header=t.CommandHeader( -# id=0xCB, subsystem=t.Subsystem.ZDO, type=t.CommandType.AREQ -# ), -# data=b"\x13\xDB\x84\x01\x21", -# ) -# -# with pytest.raises(ValueError): -# zboss.frame_received(bad_frame) -# -# +@pytest.mark.asyncio +async def test_zboss_unknown_frame(connected_zboss, caplog): + zboss, _ = connected_zboss + hl_header = t.HLCommonHeader( + version=0x0121, type=0xFFFF, id=0x123421 + ) + hl_packet = HLPacket(header=hl_header, data=t.Bytes()) + ll_header = LLHeader(flags=0xC0, size=0x0A) + frame = Frame(ll_header=ll_header, hl_packet=hl_packet) + + caplog.set_level(logging.DEBUG) + zboss.frame_received(frame) + + # Unknown frames are logged in their entirety but an error is not thrown + assert repr(frame) in caplog.text + @pytest.mark.asyncio async def test_send_failure_when_disconnected(connected_zboss): @@ -185,3 +140,48 @@ async def test_send_failure_when_disconnected(connected_zboss): assert "Coordinator is disconnected" in str(e.value) zboss.close() + + +@pytest.mark.asyncio +async def test_frame_merge(connected_zboss, mocker): + zboss, zboss_server = connected_zboss + + large_data = b"a" * (ZBNCP_LL_BODY_SIZE_MAX * 2 + 50) + command = c.NcpConfig.ReadNVRAM.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + NVRAMVersion=t.uint16_t(0x0000), + DatasetId=t.DatasetId(0x0000), + Dataset=t.NVRAMDataset(large_data), + DatasetVersion=t.uint16_t(0x0000) + ) + frame = command.to_frame() + + callback = mocker.Mock() + + zboss.register_indication_listener( + c.NcpConfig.ReadNVRAM.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + NVRAMVersion=t.uint16_t(0x0000), + DatasetId=t.DatasetId(0x0000), + Dataset=t.NVRAMDataset(large_data), + DatasetVersion=t.uint16_t(0x0000) + ), callback + ) + + # Perform fragmentation + fragments = frame.handle_tx_fragmentation() + assert len(fragments) > 1 + + # Receiving first and middle fragments + for fragment in fragments[:-1]: + assert not zboss.frame_received(fragment) + + # receiving the last fragment + assert zboss.frame_received(fragments[-1]) + + # Check the state of _rx_fragments after merging + assert zboss._rx_fragments == [] diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index 35914ba..5700753 100644 --- a/zigpy_zboss/uart.py +++ b/zigpy_zboss/uart.py @@ -96,6 +96,11 @@ def connection_lost(self, exc: typing.Optional[Exception]) -> None: if not self._reset_flag: SERIAL_LOGGER.warning( f"Unexpected connection lost... {exc}") + try: + asyncio.get_running_loop() + except RuntimeError: + LOGGER.debug("Reconnect skipped: Event loop is not running") + return self._reconnect_task = asyncio.create_task(self._reconnect()) async def _reconnect(self, timeout=RECONNECT_TIMEOUT): From bf446a4e5710a658f54c831bc69f4feee6eb92b8 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Mon, 27 May 2024 12:23:18 +0400 Subject: [PATCH 21/57] tests for api responses --- tests/api/test_response.py | 557 +++++++++++++++++++++++++++++++++++++ tests/test_commands.py | 2 +- 2 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 tests/api/test_response.py diff --git a/tests/api/test_response.py b/tests/api/test_response.py new file mode 100644 index 0000000..1cdf8aa --- /dev/null +++ b/tests/api/test_response.py @@ -0,0 +1,557 @@ +import asyncio + +import pytest +import async_timeout + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c +from zigpy_zboss.utils import deduplicate_commands + + +@pytest.mark.asyncio +async def test_responses(connected_zboss): + zboss, zboss_server = connected_zboss + + assert not any(zboss._listeners.values()) + + future = zboss.wait_for_response( + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )) + + assert any(zboss._listeners.values()) + + response = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + await zboss_server.send(response) + + assert (await future) == response + + # Our listener will have been cleaned up after a step + await asyncio.sleep(0) + assert not any(zboss._listeners.values()) + + +@pytest.mark.asyncio +async def test_responses_multiple(connected_zboss): + zboss, _ = connected_zboss + + assert not any(zboss._listeners.values()) + + future1 = zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )) + future2 = zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )) + future3 = zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )) + + response = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + zboss.frame_received(response.to_frame()) + + await future1 + await asyncio.sleep(0) + await asyncio.sleep(0) + await asyncio.sleep(0) + + assert not future2.done() + assert not future3.done() + + assert any(zboss._listeners.values()) + + +@pytest.mark.asyncio +async def test_response_timeouts(connected_zboss): + zboss, _ = connected_zboss + + response = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + + async def send_soon(delay): + await asyncio.sleep(delay) + zboss.frame_received(response.to_frame()) + + asyncio.create_task(send_soon(0.1)) + + async with async_timeout.timeout(0.5): + assert (await zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + ))) == response + + # The response was successfully received so we + # should have no outstanding listeners + await asyncio.sleep(0) + assert not any(zboss._listeners.values()) + + asyncio.create_task(send_soon(0.6)) + + with pytest.raises(asyncio.TimeoutError): + async with async_timeout.timeout(0.5): + assert ( + await zboss.wait_for_response( + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )) + ) == response + + # Our future still completed, albeit unsuccessfully. + # We should have no leaked listeners here. + assert not any(zboss._listeners.values()) + + +@pytest.mark.asyncio +async def test_response_matching_partial(connected_zboss): + zboss, _ = connected_zboss + + future = zboss.wait_for_response( + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(2), + StatusCode=20, + partial=True + ) + ) + + response1 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + response2 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(2), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + response3 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=11, + StatusCat=t.StatusCategory(2), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + + zboss.frame_received(response1.to_frame()) + zboss.frame_received(response2.to_frame()) + zboss.frame_received(response3.to_frame()) + + assert future.done() + assert (await future) == response2 + + +@pytest.mark.asyncio +async def test_response_matching_exact(connected_zboss): + zboss, _ = connected_zboss + + response1 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + response2 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(2) + ) + response3 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=11, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + + future = zboss.wait_for_response(response2) + + zboss.frame_received(response1.to_frame()) + zboss.frame_received(response2.to_frame()) + zboss.frame_received(response3.to_frame()) + + # Future should be immediately resolved + assert future.done() + assert (await future) == response2 + + +@pytest.mark.asyncio +async def test_response_not_matching_out_of_order(connected_zboss): + zboss, _ = connected_zboss + + response = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + zboss.frame_received(response.to_frame()) + + future = zboss.wait_for_response(response) + + # This future will never resolve because we were not + # expecting a response and discarded it + assert not future.done() + + +@pytest.mark.asyncio +async def test_wait_responses_empty(connected_zboss): + zboss, _ = connected_zboss + + # You shouldn't be able to wait for an empty list of responses + with pytest.raises(ValueError): + await zboss.wait_for_responses([]) + + +@pytest.mark.asyncio +async def test_response_callback_simple(connected_zboss, event_loop, mocker): + zboss, _ = connected_zboss + + sync_callback = mocker.Mock() + + good_response = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + bad_response = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=0, + DeviceRole=t.DeviceRole(1) + ) + + zboss.register_indication_listener(good_response, sync_callback) + + zboss.frame_received(bad_response.to_frame()) + assert sync_callback.call_count == 0 + + zboss.frame_received(good_response.to_frame()) + sync_callback.assert_called_once_with(good_response) + + +@pytest.mark.asyncio +async def test_response_callbacks(connected_zboss, event_loop, mocker): + zboss, _ = connected_zboss + + sync_callback = mocker.Mock() + bad_sync_callback = mocker.Mock( + side_effect=RuntimeError + ) # Exceptions should not interfere with other callbacks + + async_callback_responses = [] + + # XXX: I can't get AsyncMock().call_count to work, even though + # the callback is definitely being called + async def async_callback(response): + await asyncio.sleep(0) + async_callback_responses.append(response) + + good_response1 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + good_response2 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(2) + ) + good_response3 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ) + bad_response1 = c.ZDO.MgtLeave.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20) + bad_response2 = c.NcpConfig.GetModuleVersion.Req(TSN=1) + + responses = [ + # Duplicating matching responses shouldn't do anything + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + ), + # Matching against different response types should also work + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=4 + ), + ] + + assert set(deduplicate_commands(responses)) == { + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + ), + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ), + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=4 + ), + } + + # We shouldn't see any effects from receiving a frame early + zboss.frame_received(good_response1.to_frame()) + + for callback in [bad_sync_callback, async_callback, sync_callback]: + zboss.register_indication_listeners(responses, callback) + + zboss.frame_received(good_response1.to_frame()) + zboss.frame_received(bad_response1.to_frame()) + zboss.frame_received(good_response2.to_frame()) + zboss.frame_received(bad_response2.to_frame()) + zboss.frame_received(good_response3.to_frame()) + + await asyncio.sleep(0) + + assert sync_callback.call_count == 3 + assert bad_sync_callback.call_count == 3 + + await asyncio.sleep(0.1) + # assert async_callback.call_count == 3 # XXX: this always returns zero + assert len(async_callback_responses) == 3 + + +@pytest.mark.asyncio +async def test_wait_for_responses(connected_zboss, event_loop): + zboss, _ = connected_zboss + + response1 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ) + response2 = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(2) + ) + response3 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ) + response4 = c.ZDO.MgtLeave.Rsp(TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20) + response5 = c.NcpConfig.GetModuleVersion.Req(TSN=1) + + # We shouldn't see any effects from receiving a frame early + zboss.frame_received(response1.to_frame()) + + # Will match the first response1 and detach + future1 = zboss.wait_for_responses( + [c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + ), c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + partial=True + )] + ) + + # Will match the first response3 and detach + future2 = zboss.wait_for_responses( + [ + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(10) + ), + ] + ) + + # Will not match anything + future3 = zboss.wait_for_responses([c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=4 + )]) + + # Will match response1 the second time around + future4 = zboss.wait_for_responses( + [ + # Matching against different response types should also work + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=3 + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) + ), + c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=4 + ), + ] + ) + + zboss.frame_received(response1.to_frame()) + zboss.frame_received(response2.to_frame()) + zboss.frame_received(response3.to_frame()) + zboss.frame_received(response4.to_frame()) + zboss.frame_received(response5.to_frame()) + + assert future1.done() + assert future2.done() + assert not future3.done() + assert not future4.done() + + await asyncio.sleep(0) + + zboss.frame_received(response1.to_frame()) + zboss.frame_received(response2.to_frame()) + zboss.frame_received(response3.to_frame()) + zboss.frame_received(response4.to_frame()) + zboss.frame_received(response5.to_frame()) + + assert future1.done() + assert future2.done() + assert not future3.done() + assert future4.done() + + assert (await future1) == response1 + assert (await future2) == response3 + assert (await future4) == response1 + + await asyncio.sleep(0) + + zboss.frame_received(c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=4 + ).to_frame()) + assert future3.done() + assert (await future3) == c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + FWVersion=1, + StackVersion=2, + ProtocolVersion=4 + ) diff --git a/tests/test_commands.py b/tests/test_commands.py index 9d2fbc3..29e46e6 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -424,7 +424,7 @@ def test_command_equality(): # Different frame types do not match, even if they have the same structure assert not c.ZDO.MgtLeave.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, ).matches( + StatusCode=20).matches( c.ZDO.PermitJoin.Rsp(partial=True) ) From 4d72ada9a111df42e7a33ca6adf34b21faaca13e Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 30 May 2024 18:30:08 +0400 Subject: [PATCH 22/57] tests for application connect --- tests/api/test_connect.py | 3 +- tests/application/__init__.py | 1 + tests/application/test_connect.py | 173 ++++++++++ tests/conftest.py | 541 +++++++++++++++++++++++++++++- zigpy_zboss/api.py | 4 +- zigpy_zboss/nvram.py | 2 +- 6 files changed, 717 insertions(+), 7 deletions(-) create mode 100644 tests/application/__init__.py create mode 100644 tests/application/test_connect.py diff --git a/tests/api/test_connect.py b/tests/api/test_connect.py index f3bc685..ffc6ee6 100644 --- a/tests/api/test_connect.py +++ b/tests/api/test_connect.py @@ -27,7 +27,6 @@ async def test_api_close(connected_zboss, mocker): mocker.spy(uart, "close") # add some dummy fields and listeners, should be cleared on close - zboss.version = 2 zboss.capabilities = 4 zboss._listeners = { 'listener1': [mocker.Mock()], 'listener2': [mocker.Mock()] @@ -47,7 +46,7 @@ async def test_api_close(connected_zboss, mocker): def dict_minus(d, minus): return {k: v for k, v in d.items() if k not in minus} - ignored_keys = ["_blocking_request_lock", "nvram"] + ignored_keys = ["_blocking_request_lock", "nvram", "version"] # Closing ZBOSS should reset it completely to that of a fresh object # We have to ignore our mocked method and the lock diff --git a/tests/application/__init__.py b/tests/application/__init__.py new file mode 100644 index 0000000..944cf09 --- /dev/null +++ b/tests/application/__init__.py @@ -0,0 +1 @@ +"""Tests for application.""" diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py new file mode 100644 index 0000000..08ee86e --- /dev/null +++ b/tests/application/test_connect.py @@ -0,0 +1,173 @@ +import asyncio +import async_timeout +from unittest.mock import AsyncMock, patch + +import pytest + +import zigpy_zboss.config as conf +from zigpy_zboss.uart import connect as uart_connect +from zigpy_zboss.zigbee.application import ControllerApplication + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c + +# from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1 +from ..conftest import BaseServerZBOSS, BaseZStackDevice + +@pytest.mark.asyncio +async def test_no_double_connect(make_zboss_server, mocker): + zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) + + app = mocker.Mock() + await uart_connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + ) + + with pytest.raises(RuntimeError): + await uart_connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + ) + +@pytest.mark.asyncio +async def test_leak_detection(make_zboss_server, mocker): + zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) + + def count_connected(): + return sum(t._is_connected for t in zboss_server._transports) + + # Opening and closing one connection will keep the count at zero + assert count_connected() == 0 + app = mocker.Mock() + protocol1 = await uart_connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + ) + assert count_connected() == 1 + protocol1.close() + assert count_connected() == 0 + + # Once more for good measure + protocol2 = await uart_connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + ) + assert count_connected() == 1 + protocol2.close() + assert count_connected() == 0 + + +@pytest.mark.asyncio +async def test_probe_unsuccessful_slow(make_zboss_server, mocker): + zboss_server = make_zboss_server( + server_cls=BaseServerZBOSS, shorten_delays=False + ) + + # Don't respond to anything + zboss_server._listeners.clear() + + mocker.patch("zigpy_zboss.zigbee.application.PROBE_TIMEOUT", new=0.1) + + assert not ( + await ControllerApplication.probe( + conf.SCHEMA_DEVICE( + {conf.CONF_DEVICE_PATH: zboss_server.serial_port} + ) + ) + ) + + assert not any([t._is_connected for t in zboss_server._transports]) + + +@pytest.mark.asyncio +async def test_probe_successful(make_zboss_server, event_loop): + zboss_server = make_zboss_server( + server_cls=BaseServerZBOSS, shorten_delays=False + ) + + # This will work + ping_rsp = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1), + ) + + async def send_ping_response(): + await zboss_server.send(ping_rsp) + + event_loop.call_soon(asyncio.create_task, send_ping_response()) + + assert await ControllerApplication.probe( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}) + ) + assert not any([t._is_connected for t in zboss_server._transports]) + + +@pytest.mark.asyncio +async def test_probe_multiple(make_application): + # Make sure that our listeners don't get cleaned up after each probe + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + app.close = lambda: None + + config = conf.SCHEMA_DEVICE( + {conf.CONF_DEVICE_PATH: zboss_server.serial_port} + ) + + + assert await app.probe(config) + assert await app.probe(config) + assert await app.probe(config) + assert await app.probe(config) + + assert not any([t._is_connected for t in zboss_server._transports]) + + +@pytest.mark.asyncio +async def test_shutdown_from_app(mocker, make_application, event_loop): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + await app.startup(auto_form=False) + + # It gets deleted but we save a reference to it + transport = app._api._uart._transport + mocker.spy(transport, "close") + + # Close the connection application-side + await app.shutdown() + + # And the serial connection should have been closed + assert transport.close.call_count >= 1 + +@pytest.mark.asyncio +async def test_clean_shutdown(make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + # This should not throw + await app.shutdown() + + assert app._api is None + +@pytest.mark.asyncio +async def test_multiple_shutdown(make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + await app.shutdown() + await app.shutdown() + await app.shutdown() + + +@pytest.mark.asyncio +@patch( + "zigpy_zboss.zigbee.application.ControllerApplication._watchdog_period", + new=0.1 +) +async def test_watchdog(make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + app._watchdog_feed = AsyncMock(wraps=app._watchdog_feed) + + await app.startup(auto_form=False) + await asyncio.sleep(0.6) + assert len(app._watchdog_feed.mock_calls) >= 5 + + await app.shutdown() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 432e2b2..704cc90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,18 @@ import logging -from unittest.mock import Mock, PropertyMock, patch +from unittest.mock import Mock, PropertyMock, patch, MagicMock, AsyncMock + +from zigpy.zdo import types as zdo_t +import zigpy import zigpy_zboss.config as conf from zigpy_zboss.uart import ZbossNcpProtocol import zigpy_zboss.types as t +import zigpy_zboss.commands as c from zigpy_zboss.api import ZBOSS +from zigpy_zboss.zigbee.application import ControllerApplication +from zigpy_zboss.nvram import NVRAMHelper LOGGER = logging.getLogger(__name__) @@ -234,6 +240,31 @@ def connected_zboss(event_loop, make_connected_zboss): zboss.close() + +def reply_to(request): + def inner(function): + if not hasattr(function, "_reply_to"): + function._reply_to = [] + + function._reply_to.append(request) + + return function + + return inner + + +def serialize_zdo_command(command_id, **kwargs): + field_names, field_types = zdo_t.CLUSTERS[command_id] + + return t.Bytes(zigpy.types.serialize(kwargs.values(), field_types)) + +def deserialize_zdo_command(command_id, data): + field_names, field_types = zdo_t.CLUSTERS[command_id] + args, data = zigpy.types.deserialize(data, field_types) + + return dict(zip(field_names, args)) + + class BaseServerZBOSS(ZBOSS): align_structs = False version = None @@ -305,3 +336,511 @@ def close(self): # We don't clear listeners on shutdown with patch.object(self, "_listeners", {}): return super().close() + + + +def simple_deepcopy(d): + if not hasattr(d, "copy"): + return d + + if isinstance(d, (list, tuple)): + return type(d)(map(simple_deepcopy, d)) + elif isinstance(d, dict): + return type(d)( + {simple_deepcopy(k): simple_deepcopy(v) for k, v in d.items()} + ) + else: + return d.copy() + + +def merge_dicts(a, b): + c = simple_deepcopy(a) + + for key, value in b.items(): + if isinstance(value, dict): + c[key] = merge_dicts(c.get(key, {}), value) + else: + c[key] = value + + return c + +@pytest.fixture +def make_application(make_zboss_server): + 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 {}) + server_config = merge_dicts(default, server_config or {}) + + app = ControllerApplication(client_config) + + def add_initialized_device(self, *args, **kwargs): + device = self.add_device(*args, **kwargs) + device.status = zigpy.device.Status.ENDPOINTS_INIT + device.model = "Model" + device.manufacturer = "Manufacturer" + + device.node_desc = zdo_t.NodeDescriptor( + logical_type=zdo_t.LogicalType.Router, + complex_descriptor_available=0, + user_descriptor_available=0, + reserved=0, + aps_flags=0, + frequency_band=zdo_t.NodeDescriptor.FrequencyBand.Freq2400MHz, + mac_capability_flags=142, + manufacturer_code=4476, + maximum_buffer_size=82, + maximum_incoming_transfer_size=82, + server_mask=11264, + maximum_outgoing_transfer_size=82, + descriptor_capability_field=0, + ) + + ep = device.add_endpoint(1) + ep.status = zigpy.endpoint.Status.ZDO_INIT + + return device + + async def start_network(self): + dev = self.add_initialized_device( + ieee=t.EUI64(range(8)), nwk=0xAABB + ) + dev.model = "Coordinator Model" + dev.manufacturer = "Coordinator Manufacturer" + + dev.zdo.Mgmt_NWK_Update_req = AsyncMock( + return_value=[ + zdo_t.Status.SUCCESS, + t.Channels.ALL_CHANNELS, + 0, + 0, + [80] * 16, + ] + ) + + async def permit(self, dev): + pass + + async def energy_scan(self, channels, duration_exp, count): + return {self.state.network_info.channel: 0x1234} + + async def force_remove(self, dev): + pass + + async def add_endpoint(self, descriptor): + pass + + async def permit_ncp(self, time_s=60): + pass + + async def permit_with_link_key(self, node, link_key, time_s=60): + pass + + async def reset_network_info(self): + pass + + async def write_network_info(self, *, network_info, node_info): + pass + + async def load_network_info(self, *, load_devices=False): + self.state.network_info.channel = 15 + + app.add_initialized_device = add_initialized_device.__get__(app) + app.start_network = start_network.__get__(app) + app.permit = permit.__get__(app) + app.energy_scan = energy_scan.__get__(app) + # app.force_remove = force_remove.__get__(app) + # app.add_endpoint = add_endpoint.__get__(app) + # app.permit_ncp = permit_ncp.__get__(app) + # app.permit_with_link_key = permit_with_link_key.__get__(app) + # app.reset_network_info = reset_network_info.__get__(app) + # app.write_network_info = write_network_info.__get__(app) + # app.load_network_info = load_network_info.__get__(app) + + app.device_initialized = Mock(wraps=app.device_initialized) + app.listener_event = Mock(wraps=app.listener_event) + app.get_sequence = MagicMock(wraps=app.get_sequence, return_value=123) + app.send_packet = AsyncMock(wraps=app.send_packet) + app.write_network_info = AsyncMock(wraps=app.write_network_info) + # app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + + server = make_zboss_server( + server_cls=server_cls, config=server_config, **kwargs + ) + + return app, server + + return inner + + +class BaseZStackDevice(BaseServerZBOSS): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.active_endpoints = [] + self._nvram = {} + self._orig_nvram = {} + self.device_state = 0x00 + self.zdo_callbacks = set() + for name in dir(self): + func = getattr(self, name) + for req in getattr(func, "_reply_to", []): + self.reply_to(request=req, responses=[func]) + + + # def nvram_serialize(self, item): + # return NVRAMHelper.serialize(self, item) + # + # def nvram_deserialize(self, data, item_type): + # return NVRAMHelper.deserialize(self, data, item_type) + + # def _unhandled_command(self, command): + # LOGGER.warning("Server does not have a handler for command %s", command) + # self.send( + # c.RPCError.CommandNotRecognized.Rsp( + # ErrorCode=c.rpc_error.ErrorCode.InvalidCommandId, + # RequestHeader=command.to_frame().header, + # ) + # ) + + def connection_lost(self, exc): + self.active_endpoints.clear() + return super().connection_lost(exc) + + + @reply_to(c.NcpConfig.GetJoinStatus.Req(partial=True)) + def get_join_status(self, request): + return c.NcpConfig.GetJoinStatus.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + Joined=0x01 # Assume device is joined for this example + ) + + @reply_to(c.NcpConfig.NCPModuleReset.Req(partial=True)) + def get_ncp_reset(self, request): + return c.NcpConfig.NCPModuleReset.Rsp( + TSN=0xFF, + StatusCat=t.StatusCategory(1), + StatusCode=20 + ) + + @reply_to(c.NcpConfig.GetShortAddr.Req(partial=True)) + def get_short_addr(self, request): + return c.NcpConfig.GetShortAddr.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + NWKAddr=t.NWK(0x1234) # Example NWK address + ) + + @reply_to(c.NcpConfig.GetLocalIEEE.Req(partial=True)) + def get_local_ieee(self, request): + return c.NcpConfig.GetLocalIEEE.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + MacInterfaceNum=request.MacInterfaceNum, + IEEE=t.EUI64([0, 1, 2, 3, 4, 5, 6, 7]) # Example IEEE address + ) + + @reply_to(c.NcpConfig.GetZigbeeRole.Req(partial=True)) + def get_zigbee_role(self, request): + return c.NcpConfig.GetZigbeeRole.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + DeviceRole=t.DeviceRole(1) # Example role + ) + + @reply_to(c.NcpConfig.GetExtendedPANID.Req(partial=True)) + def get_extended_panid(self, request): + return c.NcpConfig.GetExtendedPANID.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ExtendedPANID=t.EUI64.convert("00124b0001ab89cd") # Example PAN ID + ) + + @reply_to(c.ZDO.PermitJoin.Req(partial=True)) + def get_permit_join(self, request): + return c.ZDO.PermitJoin.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ) + + @reply_to(c.NcpConfig.GetShortPANID.Req(partial=True)) + def get_short_panid(self, request): + return c.NcpConfig.GetShortPANID.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + PANID=t.PanId(0x5678) # Example short PAN ID + ) + + @reply_to(c.NcpConfig.GetCurrentChannel.Req(partial=True)) + def get_current_channel(self, request): + return c.NcpConfig.GetCurrentChannel.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + Page=0, + Channel=t.Channels(1) # Example channel + ) + + @reply_to(c.NcpConfig.GetChannelMask.Req(partial=True)) + def get_channel_mask(self, request): + return c.NcpConfig.GetChannelMask.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ChannelList=t.ChannelEntryList( + [t.ChannelEntry(page=1, channel_mask=0x07fff800)]) + ) # Example mask + + @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) + def read_nvram(self, request): + status_code = 1 + if request.DatasetId == t.DatasetId.ZB_NVRAM_COMMON_DATA: + status_code = 0 + dataset = t.DSCommonData( + byte_count=100, + bitfield=1, + depth=1, + nwk_manager_addr=0x0000, + panid=0x1234, + network_addr=0x5678, + channel_mask=t.Channels(14), + aps_extended_panid=t.EUI64.convert("00:11:22:33:44:55:66:77"), + nwk_extended_panid=t.EUI64.convert("00:11:22:33:44:55:66:77"), + parent_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + tc_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + nwk_key=t.KeyData(b'\x01' * 16), + nwk_key_seq=0, + tc_standard_key=t.KeyData(b'\x02' * 16), + channel=15, + page=0, + mac_interface_table=t.MacInterfaceTable( + bitfield_0=0, + bitfield_1=1, + link_pwr_data_rate=250, + channel_in_use=11, + supported_channels=t.Channels(15) + ), + reserved=0 + ) + nvram_version = 3 + dataset_version = 1 + elif request.DatasetId == t.DatasetId.ZB_IB_COUNTERS: + status_code = 0 + dataset = t.DSIbCounters( + byte_count=8, + nib_counter=100, # Example counter value + aib_counter=50 # Example counter value + ) + nvram_version = 1 + dataset_version = 1 + elif request.DatasetId == t.DatasetId.ZB_NVRAM_ADDR_MAP: + status_code = 0 + dataset = t.DSNwkAddrMap( + header=t.NwkAddrMapHeader( + byte_count=100, + entry_count=2, + _align=0 + ), + items=[ + t.NwkAddrMapRecord( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + nwk_addr=0x1234, + index=1, + redirect_type=0, + redirect_ref=0, + _align=0 + ), + t.NwkAddrMapRecord( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:78"), + nwk_addr=0x5678, + index=2, + redirect_type=0, + redirect_ref=0, + _align=0 + ) + ] + ) + nvram_version = 2 + dataset_version = 1 + elif request.DatasetId == t.DatasetId.ZB_NVRAM_APS_SECURE_DATA: + status_code = 0 + dataset = t.DSApsSecureKeys( + header=10, + items=[ + t.ApsSecureEntry( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + key=t.KeyData(b'\x03' * 16), + _unknown_1=0 + ), + t.ApsSecureEntry( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:78"), + key=t.KeyData(b'\x04' * 16), + _unknown_1=0 + ) + ] + ) + nvram_version = 1 + dataset_version = 1 + else: + dataset = t.NVRAMDataset(b'') + nvram_version = 1 + dataset_version = 1 + + return c.NcpConfig.ReadNVRAM.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=status_code, + NVRAMVersion=nvram_version, + DatasetId=t.DatasetId(request.DatasetId), + DatasetVersion=dataset_version, + Dataset=t.NVRAMDataset(dataset.serialize()) + ) + + @reply_to(c.NcpConfig.GetTrustCenterAddr.Req(partial=True)) + def get_trust_center_addr(self, request): + return c.NcpConfig.GetTrustCenterAddr.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + TCIEEE=t.EUI64.convert("00:11:22:33:44:55:66:77") + # Example Trust Center IEEE address + ) + + @reply_to(c.NcpConfig.GetRxOnWhenIdle.Req(partial=True)) + def get_rx_on_when_idle(self, request): + return c.NcpConfig.GetRxOnWhenIdle.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + RxOnWhenIdle=1 # Example RxOnWhenIdle value + ) + + @reply_to(c.NWK.StartWithoutFormation.Req(partial=True)) + def start_without_formation(self, request): + return c.NWK.StartWithoutFormation.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=0 # Example status code + ) + + @reply_to(c.NcpConfig.GetModuleVersion.Req(partial=True)) + def get_module_version(self, request): + return c.NcpConfig.GetModuleVersion.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, # Example status code + FWVersion=1, # Example firmware version + StackVersion=2, # Example stack version + ProtocolVersion=3 # Example protocol version + ) + + @reply_to(c.AF.SetSimpleDesc.Req(partial=True)) + def set_simple_desc(self, request): + return c.AF.SetSimpleDesc.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20 # Example status code + ) + + @reply_to(c.NcpConfig.GetEDTimeout.Req(partial=True)) + def get_ed_timeout(self, request): + return c.NcpConfig.GetEDTimeout.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + Timeout=t.TimeoutIndex(0x01) # Example timeout value + ) + + @reply_to(c.NcpConfig.GetMaxChildren.Req(partial=True)) + def get_max_children(self, request): + return c.NcpConfig.GetMaxChildren.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ChildrenNbr=5 # Example max children + ) + + @reply_to(c.NcpConfig.GetAuthenticationStatus.Req(partial=True)) + def get_authentication_status(self, request): + return c.NcpConfig.GetAuthenticationStatus.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + Authenticated=1 # Example authenticated value + ) + + @reply_to(c.NcpConfig.GetParentAddr.Req(partial=True)) + def get_parent_addr(self, request): + return c.NcpConfig.GetParentAddr.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + NWKParentAddr=t.NWK(0x1234) # Example parent NWK address + ) + + @reply_to(c.NcpConfig.GetCoordinatorVersion.Req(partial=True)) + def get_coordinator_version(self, request): + return c.NcpConfig.GetCoordinatorVersion.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=20, + CoordinatorVersion=1 # Example coordinator version + ) + + def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): + if NWKAddrOfInterest != 0x0000: + return + + responses = [ + c.ZDO.NodeDescRsp.Callback( + Src=0x0000, + Status=t.ZDOStatus.SUCCESS, + NWK=0x0000, + NodeDescriptor=c.zdo.NullableNodeDescriptor( + byte1=0, + byte2=64, + mac_capability_flags=143, + manufacturer_code=0, + maximum_buffer_size=80, + maximum_incoming_transfer_size=160, + server_mask=1, # this differs + maximum_outgoing_transfer_size=160, + descriptor_capability_field=0, + ), + ), + ] + + if zdo_t.ZDOCmd.Node_Desc_rsp in self.zdo_callbacks: + responses.append( + c.ZDO.NodeDescReq.Callback( + Src=0x0000, + IsBroadcast=t.Bool.false, + ClusterId=zdo_t.ZDOCmd.Node_Desc_rsp, + SecurityUse=0, + TSN=req.TSN, + MacDst=0x0000, + Data=serialize_zdo_command( + command_id=zdo_t.ZDOCmd.Node_Desc_rsp, + Status=t.ZDOStatus.SUCCESS, + NWKAddrOfInterest=0x0000, + NodeDescriptor=zdo_t.NodeDescriptor( + **responses[0].NodeDescriptor.as_dict() + ), + ), + ) + ) + + return responses + diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index db18480..f06b6e5 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -44,7 +44,6 @@ def __init__(self, config: conf.ConfigType): self._blocking_request_lock = asyncio.Lock() self.capabilities = None - self.version = None self.nvram = NVRAMHelper(self) self.network_info: zigpy.state.NetworkInformation = None @@ -117,7 +116,6 @@ def close(self) -> None: listener.cancel() self._listeners.clear() - self.version = None self.capabilities = None if self._uart is not None: @@ -324,7 +322,7 @@ async def version(self): req = c.NcpConfig.GetModuleVersion.Req(TSN=tsn) res = await self.request(req) if res.StatusCode: - return + return None version = ['', '', ''] for idx, ver in enumerate( [res.FWVersion, res.StackVersion, res.ProtocolVersion]): diff --git a/zigpy_zboss/nvram.py b/zigpy_zboss/nvram.py index 66fe19d..703c729 100644 --- a/zigpy_zboss/nvram.py +++ b/zigpy_zboss/nvram.py @@ -24,7 +24,7 @@ async def read(self, nv_id: t.DatasetId, item_type): ) ) if res.StatusCode != 0: - return + return None if not res.DatasetId == nv_id: raise From 98a9a0b89f1c2a616e49baae75b0811ae7375db8 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Mon, 3 Jun 2024 12:45:00 +0400 Subject: [PATCH 23/57] tests to application join operations --- tests/application/test_connect.py | 22 +++-- tests/application/test_join.py | 130 ++++++++++++++++++++++++++++++ tests/conftest.py | 62 +++++++------- 3 files changed, 177 insertions(+), 37 deletions(-) create mode 100644 tests/application/test_join.py diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index 08ee86e..5460458 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -1,5 +1,4 @@ import asyncio -import async_timeout from unittest.mock import AsyncMock, patch import pytest @@ -11,23 +10,27 @@ import zigpy_zboss.types as t import zigpy_zboss.commands as c -# from ..conftest import FORMED_DEVICES, FormedLaunchpadCC26X2R1 from ..conftest import BaseServerZBOSS, BaseZStackDevice + @pytest.mark.asyncio async def test_no_double_connect(make_zboss_server, mocker): zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) app = mocker.Mock() await uart_connect( - conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + conf.SCHEMA_DEVICE( + {conf.CONF_DEVICE_PATH: zboss_server.serial_port} + ), app ) with pytest.raises(RuntimeError): await uart_connect( - conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + conf.SCHEMA_DEVICE( + {conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app ) + @pytest.mark.asyncio async def test_leak_detection(make_zboss_server, mocker): zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) @@ -39,7 +42,8 @@ def count_connected(): assert count_connected() == 0 app = mocker.Mock() protocol1 = await uart_connect( - conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), + app ) assert count_connected() == 1 protocol1.close() @@ -47,7 +51,8 @@ def count_connected(): # Once more for good measure protocol2 = await uart_connect( - conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), app + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: zboss_server.serial_port}), + app ) assert count_connected() == 1 protocol2.close() @@ -112,7 +117,6 @@ async def test_probe_multiple(make_application): {conf.CONF_DEVICE_PATH: zboss_server.serial_port} ) - assert await app.probe(config) assert await app.probe(config) assert await app.probe(config) @@ -137,6 +141,7 @@ async def test_shutdown_from_app(mocker, make_application, event_loop): # And the serial connection should have been closed assert transport.close.call_count >= 1 + @pytest.mark.asyncio async def test_clean_shutdown(make_application): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -147,6 +152,7 @@ async def test_clean_shutdown(make_application): assert app._api is None + @pytest.mark.asyncio async def test_multiple_shutdown(make_application): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -170,4 +176,4 @@ async def test_watchdog(make_application): await asyncio.sleep(0.6) assert len(app._watchdog_feed.mock_calls) >= 5 - await app.shutdown() \ No newline at end of file + await app.shutdown() diff --git a/tests/application/test_join.py b/tests/application/test_join.py new file mode 100644 index 0000000..bca26e2 --- /dev/null +++ b/tests/application/test_join.py @@ -0,0 +1,130 @@ +import asyncio + +import pytest +import zigpy.util +import zigpy.types +import zigpy.device + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c + +from ..conftest import ( + BaseZStackDevice, + +) + + +@pytest.mark.asyncio +async def test_permit_join(mocker, make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + permit_join_coordinator = zboss_server.reply_once_to( + request=c.ZDO.PermitJoin.Req( + TSN=123, + DestNWK=t.NWK(0x0000), + PermitDuration=t.uint8_t(10), + TCSignificance=t.uint8_t(0x01), + ), + responses=[ + c.ZDO.PermitJoin.Rsp( + TSN=123, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ), + ], + ) + + await app.startup(auto_form=False) + await app.permit(time_s=10) + + assert permit_join_coordinator.done() + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_join_coordinator(make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + # Handle us opening joins on the coordinator + permit_join_coordinator = zboss_server.reply_once_to( + request=c.ZDO.PermitJoin.Req( + TSN=123, + DestNWK=t.NWK(0x0000), + PermitDuration=t.uint8_t(60), + TCSignificance=t.uint8_t(0x01), + partial=True + ), + responses=[ + c.ZDO.PermitJoin.Rsp( + TSN=123, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ), + ], + ) + + await app.startup(auto_form=False) + await app.permit(node=app.state.node_info.ieee) + + await permit_join_coordinator + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_join_device(make_application): + ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") + nwk = 0x1234 + + app, zboss_server = make_application(server_cls=BaseZStackDevice) + app.add_initialized_device(ieee=ieee, nwk=nwk) + + permit_join = zboss_server.reply_once_to( + request=c.ZDO.PermitJoin.Req( + TSN=123, + DestNWK=t.NWK(zigpy.types.t.BroadcastAddress.RX_ON_WHEN_IDLE), + PermitDuration=t.uint8_t(60), + TCSignificance=t.uint8_t(0), + ), + responses=[ + c.ZDO.PermitJoin.Rsp( + TSN=123, + StatusCat=t.StatusCategory(1), + StatusCode=20, + ) + ], + ) + + await app.startup(auto_form=False) + await app.permit(node=ieee) + + await permit_join + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_on_zdo_device_join(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.patch.object(app, "handle_join", wraps=app.handle_join) + + nwk = 0x1234 + ieee = t.EUI64.convert("11:22:33:44:55:66:77:88") + + await zboss_server.send(c.ZDO.DevAnnceInd.Ind( + NWK=nwk, + IEEE=ieee, + MacCap=t.uint8_t(0x01) + ) + ) + + await asyncio.sleep(0.1) + + app.handle_join.assert_called_once_with( + nwk=nwk, ieee=ieee, parent_nwk=None + ) + + await app.shutdown() diff --git a/tests/conftest.py b/tests/conftest.py index 704cc90..9daeaf7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ import gc import logging - from unittest.mock import Mock, PropertyMock, patch, MagicMock, AsyncMock from zigpy.zdo import types as zdo_t @@ -19,7 +18,6 @@ import zigpy_zboss.commands as c from zigpy_zboss.api import ZBOSS from zigpy_zboss.zigbee.application import ControllerApplication -from zigpy_zboss.nvram import NVRAMHelper LOGGER = logging.getLogger(__name__) @@ -65,7 +63,7 @@ def pytest_fixture_post_finalizer(fixturedef, request) -> None: @pytest.fixture def event_loop( - request: pytest.FixtureRequest, + request: pytest.FixtureRequest, ) -> typing.Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" yield asyncio.get_event_loop_policy().new_event_loop() @@ -240,7 +238,6 @@ def connected_zboss(event_loop, make_connected_zboss): zboss.close() - def reply_to(request): def inner(function): if not hasattr(function, "_reply_to"): @@ -258,6 +255,7 @@ def serialize_zdo_command(command_id, **kwargs): return t.Bytes(zigpy.types.serialize(kwargs.values(), field_types)) + def deserialize_zdo_command(command_id, data): field_names, field_types = zdo_t.CLUSTERS[command_id] args, data = zigpy.types.deserialize(data, field_types) @@ -338,7 +336,6 @@ def close(self): return super().close() - def simple_deepcopy(d): if not hasattr(d, "copy"): return d @@ -364,13 +361,14 @@ def merge_dicts(a, b): return c + @pytest.fixture def make_application(make_zboss_server): def inner( - server_cls, - client_config=None, - server_config=None, - **kwargs, + server_cls, + client_config=None, + server_config=None, + **kwargs, ): default = config_for_port_path(FAKE_SERIAL_PORT) @@ -452,7 +450,7 @@ async def load_network_info(self, *, load_devices=False): app.add_initialized_device = add_initialized_device.__get__(app) app.start_network = start_network.__get__(app) - app.permit = permit.__get__(app) + # app.permit = permit.__get__(app) app.energy_scan = energy_scan.__get__(app) # app.force_remove = force_remove.__get__(app) # app.add_endpoint = add_endpoint.__get__(app) @@ -478,6 +476,30 @@ async def load_network_info(self, *, load_devices=False): return inner +def zdo_request_matcher( + dst_addr, command_id: t.uint16_t, **kwargs +): + zdo_kwargs = {k: v for k, v in kwargs.items() if k.startswith("zdo_")} + + kwargs = {k: v for k, v in kwargs.items() if not k.startswith("zdo_")} + kwargs.setdefault("DstEndpoint", 0x00) + # kwargs.setdefault("DstPanId", 0x0000) + kwargs.setdefault("SrcEndpoint", 0x00) + kwargs.setdefault("Radius", None) + # kwargs.setdefault("Options", None) + + return c.APS.DataReq.Req( + DstAddr=t.EUI64.convert("00124b0001ab89cd"), + ClusterId=command_id, + Payload=t.Payload( + bytes([kwargs["TSN"]]) + + serialize_zdo_command(command_id, **zdo_kwargs) + ), + **kwargs, + partial=True, + ) + + class BaseZStackDevice(BaseServerZBOSS): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -491,27 +513,10 @@ def __init__(self, *args, **kwargs): for req in getattr(func, "_reply_to", []): self.reply_to(request=req, responses=[func]) - - # def nvram_serialize(self, item): - # return NVRAMHelper.serialize(self, item) - # - # def nvram_deserialize(self, data, item_type): - # return NVRAMHelper.deserialize(self, data, item_type) - - # def _unhandled_command(self, command): - # LOGGER.warning("Server does not have a handler for command %s", command) - # self.send( - # c.RPCError.CommandNotRecognized.Rsp( - # ErrorCode=c.rpc_error.ErrorCode.InvalidCommandId, - # RequestHeader=command.to_frame().header, - # ) - # ) - def connection_lost(self, exc): self.active_endpoints.clear() return super().connection_lost(exc) - @reply_to(c.NcpConfig.GetJoinStatus.Req(partial=True)) def get_join_status(self, request): return c.NcpConfig.GetJoinStatus.Rsp( @@ -601,7 +606,7 @@ def get_channel_mask(self, request): StatusCode=20, ChannelList=t.ChannelEntryList( [t.ChannelEntry(page=1, channel_mask=0x07fff800)]) - ) # Example mask + ) # Example mask @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) def read_nvram(self, request): @@ -843,4 +848,3 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): ) return responses - From f86a4cd738e351a45d412532d7f9f780bdfd1725 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 5 Jun 2024 13:57:36 +0400 Subject: [PATCH 24/57] update --- tests/application/test_requests.py | 1012 ++++++++++++++++++++++++++++ tests/conftest.py | 21 + 2 files changed, 1033 insertions(+) create mode 100644 tests/application/test_requests.py diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py new file mode 100644 index 0000000..c6d1162 --- /dev/null +++ b/tests/application/test_requests.py @@ -0,0 +1,1012 @@ +import asyncio + +import pytest +import zigpy.types as zigpy_t +import zigpy.endpoint +import zigpy.profiles +import zigpy.zdo.types as zdo_t +from zigpy.exceptions import DeliveryError + +import zigpy_zboss.types as t +import zigpy_zboss.config as conf +import zigpy_zboss.commands as c + +from ..conftest import ( + zdo_request_matcher, + serialize_zdo_command, + BaseZStackDevice +) + + +@pytest.mark.asyncio +async def test_zigpy_request(make_application): + app, zboss_server = make_application(BaseZStackDevice) + await app.startup(auto_form=False) + + # on_apsde_indication + + TSN = 1 + + device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + + ep = device.add_endpoint(1) + ep.status = zigpy.endpoint.Status.ZDO_INIT + ep.profile_id = 260 + ep.add_input_cluster(6) + + # Respond to a light turn on request + data_req = zboss_server.reply_once_to( + request=c.APS.DataReq.Req( + TSN=1, ParamLength=21, DataLength=3, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + ProfileID=260, ClusterId=6, DstEndpoint=1, + SrcEndpoint=1, Radius=0, DstAddrMode=zigpy_t.AddrMode.NWK, + Payload=t.Payload(b"\x01\x01\x01"), UseAlias=t.Bool.false, + AliasSrcAddr=t.NWK(0x0000), AliasSeqNbr=t.uint8_t(0x00), + TxOptions=c.aps.TransmitOptions.NONE, + partial=True + ), + responses=[ + c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=1, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=zigpy_t.AddrMode.NWK + ), + # c.APS.DataIndication.Ind( + # ParamLength=21, PayloadLength=None, FrameFC=None, + # SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, + # SrcEndpoint=1, ClusterId=6, ProfileId=260, + # PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, LQI=None, + # RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True + # ), + ], + ) + + # Turn on the light + await device.endpoints[1].on_off.on() + # await data_req + + await app.shutdown() + + +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_zigpy_request_failure(device, make_application, mocker): +# app, zboss_server = make_application(device) +# await app.startup(auto_form=False) +# +# TSN = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) +# +# ep = device.add_endpoint(1) +# ep.profile_id = 260 +# ep.add_input_cluster(6) +# +# # Fail to respond to a light turn on request +# zboss_server.reply_to( +# request=c.AF.DataRequestExt.Req( +# DstAddrModeAddress=t.AddrModeAddress( +# mode=t.AddrMode.NWK, address=device.nwk +# ), +# DstEndpoint=1, +# SrcEndpoint=1, +# ClusterId=6, +# TSN=TSN, +# Data=bytes([0x01, TSN, 0x01]), +# partial=True, +# ), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# c.AF.DataConfirm.Callback( +# Status=t.Status.FAILURE, +# Endpoint=1, +# TSN=TSN, +# ), +# ], +# ) +# +# mocker.spy(app, "send_packet") +# +# # Fail to turn on the light +# with pytest.raises(InvalidCommandResponse): +# await device.endpoints[1].on_off.on() +# +# assert app.send_packet.call_count == 1 +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# @pytest.mark.parametrize( +# "addr", +# [ +# t.AddrModeAddress(mode=t.AddrMode.IEEE, address=t.EUI64(range(8))), +# t.AddrModeAddress(mode=t.AddrMode.NWK, address=t.NWK(0xAABB)), +# ], +# ) +# async def test_request_addr_mode(device, addr, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) +# +# mocker.patch.object(app, "send_packet", new=CoroutineMock()) +# +# await app.request( +# device, +# use_ieee=(addr.mode == t.AddrMode.IEEE), +# profile=1, +# cluster=2, +# src_ep=3, +# dst_ep=4, +# sequence=5, +# data=b"6", +# ) +# +# assert app.send_packet.call_count == 1 +# assert app.send_packet.mock_calls[0].args[0].dst == addr.as_zigpy_type() +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_mrequest(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# +# mocker.patch.object(app, "send_packet", new=CoroutineMock()) +# group = app.groups.add_group(0x1234, "test group") +# +# await group.endpoint.on_off.on() +# +# assert app.send_packet.call_count == 1 +# assert ( +# app.send_packet.mock_calls[0].args[0].dst +# == t.AddrModeAddress(mode=t.AddrMode.Group, address=0x1234).as_zigpy_type() +# ) +# assert app.send_packet.mock_calls[0].args[0].data.serialize() == b"\x01\x01\x01" +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_mrequest_doesnt_block(device, make_application, event_loop): +# app, zboss_server = make_application(server_cls=device) +# +# zboss_server.reply_once_to( +# request=c.AF.DataRequestExt.Req( +# DstAddrModeAddress=t.AddrModeAddress(mode=t.AddrMode.Group, address=0x1234), +# ClusterId=0x0006, +# partial=True, +# ), +# responses=[ +# # Confirm the request immediately but do not send a callback response until +# # *after* the group request is "done". +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# ], +# ) +# +# data_confirm_rsp = c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS, Endpoint=1, TSN=2 +# ) +# +# request_sent = event_loop.create_future() +# request_sent.add_done_callback(lambda _: zboss_server.send(data_confirm_rsp)) +# +# await app.startup(auto_form=False) +# +# group = app.groups.add_group(0x1234, "test group") +# await group.endpoint.on_off.on() +# request_sent.set_result(True) +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_broadcast(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# await app.startup() +# +# zboss_server.reply_once_to( +# request=c.AF.DataRequestExt.Req( +# DstAddrModeAddress=t.AddrModeAddress( +# mode=t.AddrMode.Broadcast, address=0xFFFD +# ), +# DstEndpoint=0xFF, +# DstPanId=0x0000, +# SrcEndpoint=1, +# ClusterId=3, +# TSN=1, +# Radius=3, +# Data=b"???", +# partial=True, +# ), +# responses=[c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# await app.broadcast( +# profile=260, # ZHA +# cluster=0x0003, # Identify +# src_ep=1, +# dst_ep=0xFF, # Any endpoint +# grpid=0, +# radius=3, +# sequence=1, +# data=b"???", +# broadcast_address=zigpy_t.BroadcastAddress.RX_ON_WHEN_IDLE, +# ) +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_request_concurrency(device, make_application, mocker): +# app, zboss_server = make_application( +# server_cls=device, +# client_config={conf.CONF_MAX_CONCURRENT_REQUESTS: 2}, +# ) +# +# await app.startup() +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) +# +# # Keep track of how many requests we receive at once +# in_flight_requests = 0 +# did_lock = False +# +# def make_response(req): +# async def callback(req): +# nonlocal in_flight_requests +# nonlocal did_lock +# +# if app._concurrent_requests_semaphore.locked(): +# did_lock = True +# +# in_flight_requests += 1 +# assert in_flight_requests <= 2 +# +# await asyncio.sleep(0.1) +# zboss_server.send(c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS)) +# await asyncio.sleep(0.01) +# zboss_server.send( +# c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS, Endpoint=1, TSN=req.TSN +# ) +# ) +# await asyncio.sleep(0) +# +# in_flight_requests -= 1 +# assert in_flight_requests >= 0 +# +# asyncio.create_task(callback(req)) +# +# zboss_server.reply_to( +# request=c.AF.DataRequestExt.Req(partial=True), responses=[make_response] +# ) +# +# # We create a whole bunch at once +# await asyncio.gather( +# *[ +# app.request( +# device, +# profile=260, +# cluster=1, +# src_ep=1, +# dst_ep=1, +# sequence=seq, +# data=b"\x00", +# ) +# for seq in range(10) +# ] +# ) +# +# assert in_flight_requests == 0 +# assert did_lock +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_nonstandard_profile(device, make_application): +# app, zboss_server = make_application(server_cls=device) +# await app.startup(auto_form=False) +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xFA9E) +# +# ep = device.add_endpoint(2) +# ep.status = zigpy.endpoint.Status.ZDO_INIT +# ep.profile_id = 0x9876 # non-standard profile +# ep.add_input_cluster(0x0006) +# +# # Respond to a light turn on request +# data_req = zboss_server.reply_once_to( +# request=c.AF.DataRequestExt.Req( +# DstAddrModeAddress=t.AddrModeAddress( +# mode=t.AddrMode.NWK, address=device.nwk +# ), +# DstEndpoint=2, +# SrcEndpoint=1, # we default to endpoint 1 for unknown profiles +# ClusterId=0x0006, +# partial=True, +# ), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# lambda req: c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS, +# Endpoint=2, +# TSN=req.TSN, +# ), +# lambda req: c.AF.IncomingMsg.Callback( +# GroupId=0x0000, +# ClusterId=0x0006, +# SrcAddr=device.nwk, +# SrcEndpoint=2, +# DstEndpoint=1, +# WasBroadcast=t.Bool(False), +# LQI=63, +# SecurityUse=t.Bool(False), +# TimeStamp=12345678, +# TSN=0, +# Data=b"\x08" + bytes([req.TSN]) + b"\x0B\x00\x00", +# MacSrcAddr=device.nwk, +# MsgResultRadius=29, +# ), +# ], +# ) +# +# await device.endpoints[2].on_off.off() +# +# await data_req +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_request_cancellation_shielding( +# device, make_application, mocker, event_loop +# ): +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# # The data confirm timeout must be shorter than the ARSP timeout +# mocker.spy(app._zboss, "_unhandled_command") +# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) +# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) +# +# delayed_reply_sent = event_loop.create_future() +# +# def delayed_reply(req): +# async def inner(): +# # Happens after DATA_CONFIRM_TIMEOUT expires but before ARSP_TIMEOUT +# await asyncio.sleep(0.5) +# zboss_server.send( +# c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS, Endpoint=1, TSN=req.TSN +# ) +# ) +# delayed_reply_sent.set_result(True) +# +# asyncio.create_task(inner()) +# +# data_req = zboss_server.reply_once_to( +# c.AF.DataRequestExt.Req(partial=True), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# delayed_reply, +# ], +# ) +# +# with pytest.raises(asyncio.TimeoutError): +# await app.request( +# device=device, +# profile=260, +# cluster=1, +# src_ep=1, +# dst_ep=1, +# sequence=1, +# data=b"\x00", +# ) +# +# await data_req +# await delayed_reply_sent +# +# assert app._zboss._unhandled_command.call_count == 0 +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_request_recovery_route_rediscovery_zdo(device, make_application, mocker): +# TSN = 1 +# +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# # The data confirm timeout must be shorter than the ARSP timeout +# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) +# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) +# +# # Fail the first time +# route_discovered = False +# +# def route_replier(req): +# nonlocal route_discovered +# +# if not route_discovered: +# return c.ZDO.ExtRouteChk.Rsp(Status=c.zdo.RoutingStatus.FAIL) +# else: +# return c.ZDO.ExtRouteChk.Rsp(Status=c.zdo.RoutingStatus.SUCCESS) +# +# def set_route_discovered(req): +# nonlocal route_discovered +# route_discovered = True +# +# return c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS) +# +# zboss_server.reply_to( +# request=c.ZDO.ExtRouteChk.Req(Dst=device.nwk, partial=True), +# responses=[route_replier], +# override=True, +# ) +# +# was_route_discovered = zboss_server.reply_once_to( +# request=c.ZDO.ExtRouteDisc.Req( +# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True +# ), +# responses=[set_route_discovered], +# ) +# +# zdo_req = zboss_server.reply_once_to( +# request=zdo_request_matcher( +# dst_addr=t.AddrModeAddress(t.AddrMode.NWK, device.nwk), +# command_id=zdo_t.ZDOCmd.Active_EP_req, +# TSN=TSN, +# zdo_NWKAddrOfInterest=device.nwk, +# ), +# responses=[ +# c.ZDO.ActiveEpRsp.Callback( +# Src=device.nwk, +# Status=t.ZDOStatus.SUCCESS, +# NWK=device.nwk, +# ActiveEndpoints=[], +# ), +# c.ZDO.MsgCbIncoming.Callback( +# Src=device.nwk, +# IsBroadcast=t.Bool.false, +# ClusterId=zdo_t.ZDOCmd.Active_EP_rsp, +# SecurityUse=0, +# TSN=TSN, +# MacDst=device.nwk, +# Data=serialize_zdo_command( +# command_id=zdo_t.ZDOCmd.Active_EP_rsp, +# Status=t.ZDOStatus.SUCCESS, +# NWKAddrOfInterest=device.nwk, +# ActiveEPList=[], +# ), +# ), +# ], +# ) +# +# await device.zdo.Active_EP_req(device.nwk) +# +# await was_route_discovered +# await zdo_req +# +# # 6 accounts for the loopback requests +# assert sum(c.value for c in app.state.counters["Retry_NONE"].values()) == 6 + 1 +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_request_recovery_route_rediscovery_af(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# # The data confirm timeout must be shorter than the ARSP timeout +# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) +# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) +# +# # Fail the first time +# route_discovered = False +# +# def data_confirm_replier(req): +# nonlocal route_discovered +# +# return c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS if route_discovered else t.Status.NWK_NO_ROUTE, +# Endpoint=1, +# TSN=1, +# ) +# +# def set_route_discovered(req): +# nonlocal route_discovered +# route_discovered = True +# +# return c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS) +# +# was_route_discovered = zboss_server.reply_once_to( +# c.ZDO.ExtRouteDisc.Req( +# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True +# ), +# responses=[set_route_discovered], +# ) +# +# zboss_server.reply_to( +# c.AF.DataRequestExt.Req(partial=True), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# # Ignore the source routing request as well +# zboss_server.reply_to( +# c.AF.DataRequestSrcRtg.Req(partial=True), +# responses=[ +# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# await app.request( +# device=device, +# profile=260, +# cluster=1, +# src_ep=1, +# dst_ep=1, +# sequence=1, +# data=b"\x00", +# ) +# +# await was_route_discovered +# assert ( +# sum(c.value for c in app.state.counters["Retry_RouteDiscovery"].values()) == 1 +# ) +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_request_recovery_use_ieee_addr(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# # The data confirm timeout must be shorter than the ARSP timeout +# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) +# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) +# +# was_ieee_addr_used = False +# +# def data_confirm_replier(req): +# nonlocal was_ieee_addr_used +# +# if req.DstAddrModeAddress.mode == t.AddrMode.IEEE: +# status = t.Status.SUCCESS +# was_ieee_addr_used = True +# else: +# status = t.Status.MAC_NO_ACK +# +# return c.AF.DataConfirm.Callback(Status=status, Endpoint=1, TSN=1) +# +# zboss_server.reply_once_to( +# c.ZDO.ExtRouteDisc.Req( +# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True +# ), +# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# zboss_server.reply_to( +# c.AF.DataRequestExt.Req(partial=True), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# # Ignore the source routing request as well +# zboss_server.reply_to( +# c.AF.DataRequestSrcRtg.Req(partial=True), +# responses=[ +# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), +# c.AF.DataConfirm.Callback(Status=t.Status.MAC_NO_ACK, Endpoint=1, TSN=1), +# ], +# ) +# +# await app.request( +# device=device, +# profile=260, +# cluster=1, +# src_ep=1, +# dst_ep=1, +# sequence=1, +# data=b"\x00", +# ) +# +# assert was_ieee_addr_used +# assert sum(c.value for c in app.state.counters["Retry_IEEEAddress"].values()) == 1 +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device_cls", FORMED_DEVICES) +# @pytest.mark.parametrize("fw_assoc_remove", [True, False]) +# @pytest.mark.parametrize("final_status", [t.Status.SUCCESS, t.Status.APS_NO_ACK]) +# async def test_request_recovery_assoc_remove( +# device_cls, fw_assoc_remove, final_status, make_application, mocker +# ): +# app, zboss_server = make_application(server_cls=device_cls) +# +# await app.startup(auto_form=False) +# +# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) +# mocker.patch("zigpy_zboss.zigbee.application.REQUEST_ERROR_RETRY_DELAY", new=0) +# +# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) +# +# assoc_device, _ = c.util.Device.deserialize(b"\xFF" * 100) +# assoc_device.shortAddr = device.nwk +# assoc_device.nodeRelation = c.util.NodeRelation.CHILD_FFD_RX_IDLE +# +# def data_confirm_replier(req): +# bad_assoc = assoc_device +# +# return c.AF.DataConfirm.Callback( +# Status=t.Status.MAC_TRANSACTION_EXPIRED if bad_assoc else final_status, +# Endpoint=1, +# TSN=1, +# ) +# +# zboss_server.reply_to( +# c.AF.DataRequestExt.Req(partial=True), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# zboss_server.reply_to( +# c.AF.DataRequestSrcRtg.Req(partial=True), +# responses=[ +# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# def assoc_get_with_addr(req): +# nonlocal assoc_device +# +# if assoc_device is None: +# dev, _ = c.util.Device.deserialize(b"\xFF" * 100) +# return c.UTIL.AssocGetWithAddress.Rsp(Device=dev) +# +# return c.UTIL.AssocGetWithAddress.Rsp(Device=assoc_device) +# +# did_assoc_get = zboss_server.reply_once_to( +# c.UTIL.AssocGetWithAddress.Req(IEEE=device.ieee, partial=True), +# responses=[assoc_get_with_addr], +# ) +# +# if not issubclass(device_cls, FormedLaunchpadCC26X2R1): +# fw_assoc_remove = False +# +# # Not all firmwares support Add/Remove +# if fw_assoc_remove: +# +# def assoc_remove(req): +# nonlocal assoc_device +# +# if assoc_device is None: +# return c.UTIL.AssocRemove.Rsp(Status=t.Status.FAILURE) +# +# assoc_device = None +# return c.UTIL.AssocRemove.Rsp(Status=t.Status.SUCCESS) +# +# did_assoc_remove = zboss_server.reply_once_to( +# c.UTIL.AssocRemove.Req(IEEE=device.ieee), +# responses=[assoc_remove], +# ) +# +# did_assoc_add = zboss_server.reply_once_to( +# c.UTIL.AssocAdd.Req( +# NWK=device.nwk, +# IEEE=device.ieee, +# NodeRelation=c.util.NodeRelation.CHILD_FFD_RX_IDLE, +# ), +# responses=[c.UTIL.AssocAdd.Rsp(Status=t.Status.SUCCESS)], +# ) +# else: +# did_assoc_remove = None +# did_assoc_add = None +# +# was_route_discovered = zboss_server.reply_to( +# c.ZDO.ExtRouteDisc.Req( +# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True +# ), +# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# req = app.request( +# device=device, +# profile=260, +# cluster=1, +# src_ep=1, +# dst_ep=1, +# sequence=1, +# data=b"\x00", +# ) +# +# if fw_assoc_remove and final_status == t.Status.SUCCESS: +# await req +# else: +# with pytest.raises(DeliveryError): +# await req +# +# if fw_assoc_remove: +# await did_assoc_remove +# +# if final_status != t.Status.SUCCESS: +# # The association is re-added on failure +# await did_assoc_add +# else: +# assert not did_assoc_add.done() +# elif issubclass(device_cls, FormedLaunchpadCC26X2R1): +# await did_assoc_get +# assert was_route_discovered.call_count >= 1 +# else: +# # Don't even attempt this with older firmwares +# assert not did_assoc_get.done() +# assert was_route_discovered.call_count == 0 +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# @pytest.mark.parametrize("succeed", [True, False]) +# @pytest.mark.parametrize("relays", [[0x1111, 0x2222, 0x3333], []]) +# async def test_request_recovery_manual_source_route( +# device, succeed, relays, make_application, mocker +# ): +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) +# mocker.patch("zigpy_zboss.zigbee.application.REQUEST_ERROR_RETRY_DELAY", new=0) +# +# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# +# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) +# device.relays = relays +# +# def data_confirm_replier(req): +# if isinstance(req, c.AF.DataRequestExt.Req) or not succeed: +# return c.AF.DataConfirm.Callback( +# Status=t.Status.MAC_NO_ACK, +# Endpoint=1, +# TSN=1, +# ) +# else: +# return c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS, +# Endpoint=1, +# TSN=1, +# ) +# +# normal_data_request = zboss_server.reply_to( +# c.AF.DataRequestExt.Req(partial=True), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# source_routing_data_request = zboss_server.reply_to( +# c.AF.DataRequestSrcRtg.Req(partial=True), +# responses=[ +# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), +# data_confirm_replier, +# ], +# ) +# +# zboss_server.reply_to( +# c.ZDO.ExtRouteDisc.Req( +# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True +# ), +# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# req = app.request( +# device=device, +# profile=260, +# cluster=1, +# src_ep=1, +# dst_ep=1, +# sequence=1, +# data=b"\x00", +# ) +# +# if succeed: +# await req +# else: +# with pytest.raises(DeliveryError): +# await req +# +# # In either case only one source routing attempt is performed +# assert source_routing_data_request.call_count == 1 +# assert normal_data_request.call_count >= 1 +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +# async def test_route_discovery_concurrency(device, make_application): +# app, zboss_server = make_application(server_cls=device) +# +# await app.startup(auto_form=False) +# +# route_discovery1 = zboss_server.reply_to( +# c.ZDO.ExtRouteDisc.Req(Dst=0x1234, partial=True), +# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# route_discovery2 = zboss_server.reply_to( +# c.ZDO.ExtRouteDisc.Req(Dst=0x5678, partial=True), +# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# await asyncio.gather( +# app._discover_route(0x1234), +# app._discover_route(0x5678), +# app._discover_route(0x1234), +# app._discover_route(0x5678), +# app._discover_route(0x5678), +# app._discover_route(0x5678), +# app._discover_route(0x1234), +# ) +# +# assert route_discovery1.call_count == 1 +# assert route_discovery2.call_count == 1 +# +# await app._discover_route(0x5678) +# +# assert route_discovery1.call_count == 1 +# assert route_discovery2.call_count == 2 +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_send_security_and_packet_source_route(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# await app.startup(auto_form=False) +# +# packet = zigpy_t.ZigbeePacket( +# src=zigpy_t.AddrModeAddress( +# addr_mode=zigpy_t.AddrMode.NWK, address=app.state.node_info.nwk +# ), +# src_ep=0x9A, +# dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), +# dst_ep=0xBC, +# tsn=0xDE, +# profile_id=0x1234, +# cluster_id=0x0006, +# data=zigpy_t.SerializableBytes(b"test data"), +# extended_timeout=False, +# tx_options=( +# zigpy_t.TransmitOptions.ACK | zigpy_t.TransmitOptions.APS_Encryption +# ), +# source_route=[0xAABB, 0xCCDD], +# ) +# +# data_req = zboss_server.reply_once_to( +# request=c.AF.DataRequestSrcRtg.Req( +# DstAddr=packet.dst.address, +# DstEndpoint=packet.dst_ep, +# # SrcEndpoint=packet.src_ep, +# ClusterId=packet.cluster_id, +# TSN=packet.tsn, +# Data=packet.data.serialize(), +# SourceRoute=packet.source_route, +# partial=True, +# ), +# responses=[ +# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), +# c.AF.DataConfirm.Callback( +# Status=t.Status.SUCCESS, +# Endpoint=packet.dst_ep, +# TSN=packet.tsn, +# ), +# ], +# ) +# +# await app.send_packet(packet) +# req = await data_req +# assert c.af.TransmitOptions.ENABLE_SECURITY in req.Options +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_send_packet_failure(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# await app.startup(auto_form=False) +# +# packet = zigpy_t.ZigbeePacket( +# src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), +# src_ep=0x9A, +# dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), +# dst_ep=0xBC, +# tsn=0xDE, +# profile_id=0x1234, +# cluster_id=0x0006, +# data=zigpy_t.SerializableBytes(b"test data"), +# ) +# +# zboss_server.reply_to( +# request=c.ZDO.ExtRouteDisc.Req(Dst=packet.dst.address, partial=True), +# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], +# ) +# +# zboss_server.reply_to( +# request=c.AF.DataRequestExt.Req(partial=True), +# responses=[ +# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), +# c.AF.DataConfirm.Callback( +# Status=t.Status.MAC_NO_ACK, +# Endpoint=packet.dst_ep, +# TSN=packet.tsn, +# ), +# ], +# ) +# +# with pytest.raises(zigpy.exceptions.DeliveryError) as excinfo: +# await app.send_packet(packet) +# +# assert excinfo.value.status == t.Status.MAC_NO_ACK +# +# await app.shutdown() +# +# +# @pytest.mark.parametrize("device", FORMED_DEVICES) +# async def test_send_packet_failure_disconnected(device, make_application, mocker): +# app, zboss_server = make_application(server_cls=device) +# await app.startup(auto_form=False) +# +# app._zboss = None +# +# packet = zigpy_t.ZigbeePacket( +# src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), +# src_ep=0x9A, +# dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), +# dst_ep=0xBC, +# tsn=0xDE, +# profile_id=0x1234, +# cluster_id=0x0006, +# data=zigpy_t.SerializableBytes(b"test data"), +# ) +# +# with pytest.raises(zigpy.exceptions.DeliveryError) as excinfo: +# await app.send_packet(packet) +# +# assert "Coordinator is disconnected" in str(excinfo.value) +# +# await app.shutdown() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 9daeaf7..4c575cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -543,6 +543,27 @@ def get_short_addr(self, request): NWKAddr=t.NWK(0x1234) # Example NWK address ) + @reply_to(c.APS.DataReq.Req(partial=True, DstEndpoint=0)) + def on_zdo_request(self, req): + kwargs = deserialize_zdo_command(req.ClusterId, req.Data[1:]) + handler_name = f"on_zdo_{zdo_t.ZDOCmd(req.ClusterId).name.lower()}" + handler = getattr(self, handler_name, None) + + if handler is None: + LOGGER.warning( + "No ZDO handler %s, kwargs: %s", + handler_name, kwargs + ) + return + + responses = handler(req=req, **kwargs) or [] + + return [c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(1), + StatusCode=0 + )] + responses + @reply_to(c.NcpConfig.GetLocalIEEE.Req(partial=True)) def get_local_ieee(self, request): return c.NcpConfig.GetLocalIEEE.Rsp( From 492f257f478be0611cd290b83bb214fd8ba04712 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 5 Jun 2024 15:39:55 +0400 Subject: [PATCH 25/57] update --- tests/application/test_requests.py | 64 +++++++++++++++--------------- tests/conftest.py | 43 +++++++++++--------- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index c6d1162..5c6e256 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -35,41 +35,41 @@ async def test_zigpy_request(make_application): ep.add_input_cluster(6) # Respond to a light turn on request - data_req = zboss_server.reply_once_to( - request=c.APS.DataReq.Req( - TSN=1, ParamLength=21, DataLength=3, - DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), - ProfileID=260, ClusterId=6, DstEndpoint=1, - SrcEndpoint=1, Radius=0, DstAddrMode=zigpy_t.AddrMode.NWK, - Payload=t.Payload(b"\x01\x01\x01"), UseAlias=t.Bool.false, - AliasSrcAddr=t.NWK(0x0000), AliasSeqNbr=t.uint8_t(0x00), - TxOptions=c.aps.TransmitOptions.NONE, - partial=True - ), - responses=[ - c.APS.DataReq.Rsp( - TSN=1, - StatusCat=t.StatusCategory(4), - StatusCode=1, - DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), - DstEndpoint=1, - SrcEndpoint=1, - TxTime=1, - DstAddrMode=zigpy_t.AddrMode.NWK - ), - # c.APS.DataIndication.Ind( - # ParamLength=21, PayloadLength=None, FrameFC=None, - # SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, - # SrcEndpoint=1, ClusterId=6, ProfileId=260, - # PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, LQI=None, - # RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True - # ), - ], - ) + # data_req = zboss_server.reply_once_to( + # request=c.APS.DataReq.Req( + # TSN=1, ParamLength=21, DataLength=3, + # DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + # ProfileID=260, ClusterId=6, DstEndpoint=1, + # SrcEndpoint=1, Radius=0, DstAddrMode=zigpy_t.AddrMode.NWK, + # Payload=t.Payload(b"\x01\x01\x01"), UseAlias=t.Bool.false, + # AliasSrcAddr=t.NWK(0x0000), AliasSeqNbr=t.uint8_t(0x00), + # TxOptions=c.aps.TransmitOptions.NONE, + # partial=True + # ), + # responses=[ + # c.APS.DataReq.Rsp( + # TSN=1, + # StatusCat=t.StatusCategory(4), + # StatusCode=1, + # DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + # DstEndpoint=1, + # SrcEndpoint=1, + # TxTime=1, + # DstAddrMode=zigpy_t.AddrMode.NWK + # ), + # # c.APS.DataIndication.Ind( + # # ParamLength=21, PayloadLength=None, FrameFC=None, + # # SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, + # # SrcEndpoint=1, ClusterId=6, ProfileId=260, + # # PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, LQI=None, + # # RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True + # # ), + # ], + # ) # Turn on the light await device.endpoints[1].on_off.on() - # await data_req + #await data_req await app.shutdown() diff --git a/tests/conftest.py b/tests/conftest.py index 4c575cf..739b76f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -543,26 +543,31 @@ def get_short_addr(self, request): NWKAddr=t.NWK(0x1234) # Example NWK address ) - @reply_to(c.APS.DataReq.Req(partial=True, DstEndpoint=0)) + @reply_to(c.APS.DataReq.Req(partial=True)) def on_zdo_request(self, req): - kwargs = deserialize_zdo_command(req.ClusterId, req.Data[1:]) - handler_name = f"on_zdo_{zdo_t.ZDOCmd(req.ClusterId).name.lower()}" - handler = getattr(self, handler_name, None) - - if handler is None: - LOGGER.warning( - "No ZDO handler %s, kwargs: %s", - handler_name, kwargs - ) - return - - responses = handler(req=req, **kwargs) or [] - - return [c.APS.DataReq.Rsp( - TSN=1, - StatusCat=t.StatusCategory(1), - StatusCode=0 - )] + responses + # kwargs = deserialize_zdo_command(req.ClusterId, req.Payload) + # handler_name = f"on_zdo_{zdo_t.ZDOCmd(req.ClusterId).name.lower()}" + # handler = getattr(self, handler_name, None) + # + # if handler is None: + # LOGGER.warning( + # "No ZDO handler %s, kwargs: %s", + # handler_name, kwargs + # ) + # return + # + # responses = handler(req=req, **kwargs) or [] + + return c.APS.DataReq.Rsp( + TSN=req.TSN, + StatusCat=t.StatusCategory(4), + StatusCode=20, + DstAddr=req.DstAddr, + DstEndpoint=req.DstEndpoint, + SrcEndpoint=req.SrcEndpoint, + TxTime=1, + DstAddrMode=zigpy.types.AddrMode.NWK + ) @reply_to(c.NcpConfig.GetLocalIEEE.Req(partial=True)) def get_local_ieee(self, request): From 46ca9be2c7d223c4f6b34bc9dcff24dd8d42cbf2 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 12 Jun 2024 16:33:12 +0400 Subject: [PATCH 26/57] application requests tests --- tests/application/test_requests.py | 610 +++++++++++++++-------------- tests/conftest.py | 6 +- tests/test_types_named.py | 2 +- zigpy_zboss/zigbee/application.py | 4 +- 4 files changed, 317 insertions(+), 305 deletions(-) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 5c6e256..0ae3c1c 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -1,6 +1,7 @@ import asyncio import pytest +from unittest.mock import AsyncMock as CoroutineMock import zigpy.types as zigpy_t import zigpy.endpoint import zigpy.profiles @@ -23,10 +24,6 @@ async def test_zigpy_request(make_application): app, zboss_server = make_application(BaseZStackDevice) await app.startup(auto_form=False) - # on_apsde_indication - - TSN = 1 - device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) ep = device.add_endpoint(1) @@ -34,42 +31,57 @@ async def test_zigpy_request(make_application): ep.profile_id = 260 ep.add_input_cluster(6) + # Construct the payload with the correct FrameControl byte + # FrameControl bits: 0001 0000 -> 0x10 for Server_to_Client + frame_control_byte = 0x18 + tsn = 0x01 + command_id = 0x01 + + payload = [frame_control_byte, tsn, command_id] + payload_length = len(payload) # Respond to a light turn on request - # data_req = zboss_server.reply_once_to( - # request=c.APS.DataReq.Req( - # TSN=1, ParamLength=21, DataLength=3, - # DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), - # ProfileID=260, ClusterId=6, DstEndpoint=1, - # SrcEndpoint=1, Radius=0, DstAddrMode=zigpy_t.AddrMode.NWK, - # Payload=t.Payload(b"\x01\x01\x01"), UseAlias=t.Bool.false, - # AliasSrcAddr=t.NWK(0x0000), AliasSeqNbr=t.uint8_t(0x00), - # TxOptions=c.aps.TransmitOptions.NONE, - # partial=True - # ), - # responses=[ - # c.APS.DataReq.Rsp( - # TSN=1, - # StatusCat=t.StatusCategory(4), - # StatusCode=1, - # DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), - # DstEndpoint=1, - # SrcEndpoint=1, - # TxTime=1, - # DstAddrMode=zigpy_t.AddrMode.NWK - # ), - # # c.APS.DataIndication.Ind( - # # ParamLength=21, PayloadLength=None, FrameFC=None, - # # SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, - # # SrcEndpoint=1, ClusterId=6, ProfileId=260, - # # PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, LQI=None, - # # RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True - # # ), - # ], - # ) + zboss_server.reply_once_to( + request=c.APS.DataReq.Req( + TSN=1, ParamLength=21, DataLength=3, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + ProfileID=260, ClusterId=6, DstEndpoint=1, SrcEndpoint=1, Radius=0, + DstAddrMode=zigpy_t.AddrMode.NWK, + TxOptions=c.aps.TransmitOptions.NONE, + UseAlias=t.Bool.false, AliasSrcAddr=0x0000, AliasSeqNbr=0, + Payload=[1, 1, 1]), + responses=[c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=1, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=zigpy_t.AddrMode.NWK + ), + c.APS.DataIndication.Ind( + ParamLength=21, + PayloadLength=payload_length, + FrameFC=t.APSFrameFC(0x01), + SrcAddr=t.NWK(0xAABB), + DstAddr=t.NWK(0x1234), + GrpAddr=t.NWK(0x5678), + DstEndpoint=1, + SrcEndpoint=1, + ClusterId=6, + ProfileId=260, + PacketCounter=10, + SrcMACAddr=t.NWK(0xAABB), + DstMACAddr=t.NWK(0x1234), + LQI=255, + RSSI=-70, + KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(payload) + )], + ) # Turn on the light await device.endpoints[1].on_off.on() - #await data_req await app.shutdown() @@ -120,196 +132,235 @@ async def test_zigpy_request(make_application): # await app.shutdown() # # -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# @pytest.mark.parametrize( -# "addr", -# [ -# t.AddrModeAddress(mode=t.AddrMode.IEEE, address=t.EUI64(range(8))), -# t.AddrModeAddress(mode=t.AddrMode.NWK, address=t.NWK(0xAABB)), -# ], -# ) -# async def test_request_addr_mode(device, addr, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) -# -# mocker.patch.object(app, "send_packet", new=CoroutineMock()) -# -# await app.request( -# device, -# use_ieee=(addr.mode == t.AddrMode.IEEE), -# profile=1, -# cluster=2, -# src_ep=3, -# dst_ep=4, -# sequence=5, -# data=b"6", -# ) -# -# assert app.send_packet.call_count == 1 -# assert app.send_packet.mock_calls[0].args[0].dst == addr.as_zigpy_type() -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# async def test_mrequest(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# -# mocker.patch.object(app, "send_packet", new=CoroutineMock()) -# group = app.groups.add_group(0x1234, "test group") -# -# await group.endpoint.on_off.on() -# -# assert app.send_packet.call_count == 1 -# assert ( -# app.send_packet.mock_calls[0].args[0].dst -# == t.AddrModeAddress(mode=t.AddrMode.Group, address=0x1234).as_zigpy_type() -# ) -# assert app.send_packet.mock_calls[0].args[0].data.serialize() == b"\x01\x01\x01" -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_mrequest_doesnt_block(device, make_application, event_loop): -# app, zboss_server = make_application(server_cls=device) -# -# zboss_server.reply_once_to( -# request=c.AF.DataRequestExt.Req( -# DstAddrModeAddress=t.AddrModeAddress(mode=t.AddrMode.Group, address=0x1234), -# ClusterId=0x0006, -# partial=True, -# ), -# responses=[ -# # Confirm the request immediately but do not send a callback response until -# # *after* the group request is "done". -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# ], -# ) -# -# data_confirm_rsp = c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS, Endpoint=1, TSN=2 -# ) -# -# request_sent = event_loop.create_future() -# request_sent.add_done_callback(lambda _: zboss_server.send(data_confirm_rsp)) -# -# await app.startup(auto_form=False) -# -# group = app.groups.add_group(0x1234, "test group") -# await group.endpoint.on_off.on() -# request_sent.set_result(True) -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_broadcast(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# await app.startup() -# -# zboss_server.reply_once_to( -# request=c.AF.DataRequestExt.Req( -# DstAddrModeAddress=t.AddrModeAddress( -# mode=t.AddrMode.Broadcast, address=0xFFFD -# ), -# DstEndpoint=0xFF, -# DstPanId=0x0000, -# SrcEndpoint=1, -# ClusterId=3, -# TSN=1, -# Radius=3, -# Data=b"???", -# partial=True, -# ), -# responses=[c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# await app.broadcast( -# profile=260, # ZHA -# cluster=0x0003, # Identify -# src_ep=1, -# dst_ep=0xFF, # Any endpoint -# grpid=0, -# radius=3, -# sequence=1, -# data=b"???", -# broadcast_address=zigpy_t.BroadcastAddress.RX_ON_WHEN_IDLE, -# ) -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_request_concurrency(device, make_application, mocker): -# app, zboss_server = make_application( -# server_cls=device, -# client_config={conf.CONF_MAX_CONCURRENT_REQUESTS: 2}, -# ) -# -# await app.startup() -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) -# -# # Keep track of how many requests we receive at once -# in_flight_requests = 0 -# did_lock = False -# -# def make_response(req): -# async def callback(req): -# nonlocal in_flight_requests -# nonlocal did_lock -# -# if app._concurrent_requests_semaphore.locked(): -# did_lock = True -# -# in_flight_requests += 1 -# assert in_flight_requests <= 2 -# -# await asyncio.sleep(0.1) -# zboss_server.send(c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS)) -# await asyncio.sleep(0.01) -# zboss_server.send( -# c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS, Endpoint=1, TSN=req.TSN -# ) -# ) -# await asyncio.sleep(0) -# -# in_flight_requests -= 1 -# assert in_flight_requests >= 0 -# -# asyncio.create_task(callback(req)) -# -# zboss_server.reply_to( -# request=c.AF.DataRequestExt.Req(partial=True), responses=[make_response] -# ) -# -# # We create a whole bunch at once -# await asyncio.gather( -# *[ -# app.request( -# device, -# profile=260, -# cluster=1, -# src_ep=1, -# dst_ep=1, -# sequence=seq, -# data=b"\x00", -# ) -# for seq in range(10) -# ] -# ) -# -# assert in_flight_requests == 0 -# assert did_lock -# -# await app.shutdown() -# -# + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "addr", + [ + zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.IEEE, address=t.EUI64(range(8))), + zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.NWK, address=t.NWK(0xAABB)), + ], +) +async def test_request_addr_mode(addr, make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + await app.startup(auto_form=False) + + device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + + mocker.patch.object(app, "send_packet", new=CoroutineMock()) + + await app.request( + device, + use_ieee=(addr.addr_mode == zigpy.types.AddrMode.IEEE), + profile=1, + cluster=2, + src_ep=3, + dst_ep=4, + sequence=5, + data=b"6", + ) + + assert app.send_packet.call_count == 1 + assert app.send_packet.mock_calls[0].args[0].dst == addr + + await app.shutdown() + +@pytest.mark.asyncio +async def test_mrequest(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + mocker.patch.object(app, "send_packet", new=CoroutineMock()) + group = app.groups.add_group(0x1234, "test group") + + await group.endpoint.on_off.on() + + assert app.send_packet.call_count == 1 + assert ( + app.send_packet.mock_calls[0].args[0].dst + == zigpy.types.AddrModeAddress( + addr_mode=zigpy.types.AddrMode.Group, address=0x1234 + ) + ) + assert app.send_packet.mock_calls[0].args[0].data.serialize() == b"\x01\x01\x01" + + await app.shutdown() + +@pytest.mark.asyncio +async def test_mrequest_doesnt_block(make_application, event_loop): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + zboss_server.reply_once_to( + request=c.APS.DataReq.Req( + TSN=1, ParamLength=21, DataLength=3, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:12:34"), + ProfileID=260, ClusterId=6, DstEndpoint=0, SrcEndpoint=1, Radius=0, + DstAddrMode=zigpy_t.AddrMode.Group, + TxOptions=c.aps.TransmitOptions.NONE, + UseAlias=t.Bool.false, AliasSrcAddr=0x0000, AliasSeqNbr=0, + Payload=[1, 1, 1]), + responses=[ + c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(1), + StatusCode=0, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=zigpy_t.AddrMode.Group, + ), + ], + ) + + data_confirm_rsp = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=None, FrameFC=None, + SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, + SrcEndpoint=1, ClusterId=6, ProfileId=260, + PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, + LQI=None, RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True + ) + + request_sent = event_loop.create_future() + async def on_request_sent(): + await zboss_server.send(data_confirm_rsp) + + request_sent.add_done_callback( + lambda _: event_loop.create_task(on_request_sent()) + ) + + await app.startup(auto_form=False) + + group = app.groups.add_group(0x1234, "test group") + await group.endpoint.on_off.on() + request_sent.set_result(True) + + await app.shutdown() + +@pytest.mark.asyncio +async def test_broadcast(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup() + zboss_server.reply_once_to( + request=c.APS.DataReq.Req(TSN=1, ParamLength=21, DataLength=3, + DstAddr=t.EUI64.convert( + "00:00:00:00:00:00:ff:fd"), + ProfileID=260, ClusterId=3, DstEndpoint=255, + SrcEndpoint=1, Radius=3, + DstAddrMode=zigpy_t.AddrMode.Group, + TxOptions=c.aps.TransmitOptions.NONE, + UseAlias=t.Bool.false, + AliasSrcAddr=0x0000, AliasSeqNbr=0, + Payload=[63, 63, 63]), + responses=[ + c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(1), + StatusCode=0, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:ff:fd"), + DstEndpoint=255, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=zigpy_t.AddrMode.Group, + ), + ], + ) + + await app.broadcast( + profile=260, # ZHA + cluster=0x0003, # Identify + src_ep=1, + dst_ep=0xFF, # Any endpoint + grpid=0, + radius=3, + sequence=1, + data=b"???", + broadcast_address=zigpy_t.BroadcastAddress.RX_ON_WHEN_IDLE, + ) + + await app.shutdown() + +@pytest.mark.asyncio +async def test_request_concurrency(make_application, mocker): + app, zboss_server = make_application( + server_cls=BaseZStackDevice, + client_config={conf.CONF_MAX_CONCURRENT_REQUESTS: 2}, + ) + + await app.startup() + + device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + + # Keep track of how many requests we receive at once + in_flight_requests = 0 + did_lock = False + + def make_response(req): + async def callback(req): + nonlocal in_flight_requests + nonlocal did_lock + + if app._concurrent_requests_semaphore.locked(): + did_lock = True + + in_flight_requests += 1 + assert in_flight_requests <= 10 + + await asyncio.sleep(0.1) + await zboss_server.send(c.APS.DataReq.Rsp( + TSN=req.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=0, + DstAddr=req.DstAddr, + DstEndpoint=req.DstEndpoint, + SrcEndpoint=req.SrcEndpoint, + TxTime=1, + DstAddrMode=req.DstAddrMode, + )) + await asyncio.sleep(0.01) + await zboss_server.send( + c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=None, FrameFC=None, + SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, + SrcEndpoint=1, ClusterId=6, ProfileId=260, + PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, + LQI=None, + RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True + ) + ) + await asyncio.sleep(0) + + in_flight_requests -= 1 + assert in_flight_requests >= 0 + + asyncio.create_task(callback(req)) + + zboss_server.reply_to( + request=c.APS.DataReq.Req(partial=True), responses=[make_response] + ) + + # We create a whole bunch at once + await asyncio.gather( + *[ + app.request( + device, + profile=260, + cluster=1, + src_ep=1, + dst_ep=1, + sequence=seq, + data=b"\x00", + ) + for seq in range(10) + ] + ) + + assert in_flight_requests == 0 + assert did_lock + + await app.shutdown() + + # @pytest.mark.parametrize("device", FORMED_DEVICES) # async def test_nonstandard_profile(device, make_application): # app, zboss_server = make_application(server_cls=device) @@ -374,9 +425,9 @@ async def test_zigpy_request(make_application): # await app.startup(auto_form=False) # # # The data confirm timeout must be shorter than the ARSP timeout -# mocker.spy(app._zboss, "_unhandled_command") +# mocker.spy(app._api, "_unhandled_command") # mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 # # device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) # @@ -417,7 +468,7 @@ async def test_zigpy_request(make_application): # await data_req # await delayed_reply_sent # -# assert app._zboss._unhandled_command.call_count == 0 +# assert app._api._unhandled_command.call_count == 0 # # await app.shutdown() # @@ -432,7 +483,7 @@ async def test_zigpy_request(make_application): # # # The data confirm timeout must be shorter than the ARSP timeout # mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 # # device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) # @@ -516,7 +567,7 @@ async def test_zigpy_request(make_application): # # # The data confirm timeout must be shorter than the ARSP timeout # mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 # # device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) # @@ -588,7 +639,7 @@ async def test_zigpy_request(make_application): # # # The data confirm timeout must be shorter than the ARSP timeout # mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 # # device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) # @@ -658,7 +709,7 @@ async def test_zigpy_request(make_application): # mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) # mocker.patch("zigpy_zboss.zigbee.application.REQUEST_ERROR_RETRY_DELAY", new=0) # -# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 # # device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) # @@ -792,7 +843,7 @@ async def test_zigpy_request(make_application): # mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) # mocker.patch("zigpy_zboss.zigbee.application.REQUEST_ERROR_RETRY_DELAY", new=0) # -# app._zboss._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 +# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 # # device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) # device.relays = relays @@ -945,68 +996,29 @@ async def test_zigpy_request(make_application): # await app.shutdown() # # -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# async def test_send_packet_failure(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# await app.startup(auto_form=False) -# -# packet = zigpy_t.ZigbeePacket( -# src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), -# src_ep=0x9A, -# dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), -# dst_ep=0xBC, -# tsn=0xDE, -# profile_id=0x1234, -# cluster_id=0x0006, -# data=zigpy_t.SerializableBytes(b"test data"), -# ) -# -# zboss_server.reply_to( -# request=c.ZDO.ExtRouteDisc.Req(Dst=packet.dst.address, partial=True), -# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# zboss_server.reply_to( -# request=c.AF.DataRequestExt.Req(partial=True), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# c.AF.DataConfirm.Callback( -# Status=t.Status.MAC_NO_ACK, -# Endpoint=packet.dst_ep, -# TSN=packet.tsn, -# ), -# ], -# ) -# -# with pytest.raises(zigpy.exceptions.DeliveryError) as excinfo: -# await app.send_packet(packet) -# -# assert excinfo.value.status == t.Status.MAC_NO_ACK -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# async def test_send_packet_failure_disconnected(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# await app.startup(auto_form=False) -# -# app._zboss = None -# -# packet = zigpy_t.ZigbeePacket( -# src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), -# src_ep=0x9A, -# dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), -# dst_ep=0xBC, -# tsn=0xDE, -# profile_id=0x1234, -# cluster_id=0x0006, -# data=zigpy_t.SerializableBytes(b"test data"), -# ) -# -# with pytest.raises(zigpy.exceptions.DeliveryError) as excinfo: -# await app.send_packet(packet) -# -# assert "Coordinator is disconnected" in str(excinfo.value) -# -# await app.shutdown() \ No newline at end of file + + +@pytest.mark.asyncio +async def test_send_packet_failure_disconnected(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + app._api = None + + packet = zigpy_t.ZigbeePacket( + src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), + src_ep=0x9A, + dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), + dst_ep=0xBC, + tsn=0xDE, + profile_id=0x1234, + cluster_id=0x0006, + data=zigpy_t.SerializableBytes(b"test data"), + ) + + with pytest.raises(zigpy.exceptions.DeliveryError) as excinfo: + await app.send_packet(packet) + + assert "Coordinator is disconnected" in str(excinfo.value) + + await app.shutdown() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 739b76f..c4dc62b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -560,13 +560,13 @@ def on_zdo_request(self, req): return c.APS.DataReq.Rsp( TSN=req.TSN, - StatusCat=t.StatusCategory(4), - StatusCode=20, + StatusCat=t.StatusCategory(1), + StatusCode=0, DstAddr=req.DstAddr, DstEndpoint=req.DstEndpoint, SrcEndpoint=req.SrcEndpoint, TxTime=1, - DstAddrMode=zigpy.types.AddrMode.NWK + DstAddrMode=req.DstAddrMode, ) @reply_to(c.NcpConfig.GetLocalIEEE.Req(partial=True)) diff --git a/tests/test_types_named.py b/tests/test_types_named.py index 443892e..31cc0da 100644 --- a/tests/test_types_named.py +++ b/tests/test_types_named.py @@ -27,7 +27,7 @@ def test_channel_entry(): assert channel_entry != t.ChannelEntry(page=0, channel_mask=0x0200) # Test __repr__ - expected_repr = "ChannelEntry(page=1, channels=)" + expected_repr = "ChannelEntry(page=1, channels=)" assert repr(channel_entry) == expected_repr # Test handling of None types for page or channel_mask diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index d3dd31b..7fee917 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -682,8 +682,8 @@ async def send_packet(self, packet: t.ZigbeePacket) -> None: DstAddr=dst_addr, ProfileID=packet.profile_id, ClusterId=packet.cluster_id, - DstEndpoint=packet.dst_ep, - SrcEndpoint=packet.src_ep, + DstEndpoint=packet.dst_ep or 0, + SrcEndpoint=packet.src_ep or 0, Radius=packet.radius or 0, DstAddrMode=dst_addr_mode, TxOptions=options, From 0a0e4eb53adccdd4a8e16b0af0cee1dd2bd7792d Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 12 Jun 2024 18:03:44 +0400 Subject: [PATCH 27/57] fix request concurrency test --- tests/application/test_requests.py | 22 +++++++++------------- tests/conftest.py | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 0ae3c1c..be3d952 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -291,6 +291,11 @@ async def test_request_concurrency(make_application, mocker): device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + ep = device.add_endpoint(1) + ep.status = zigpy.endpoint.Status.ZDO_INIT + ep.profile_id = 260 + ep.add_input_cluster(6) + # Keep track of how many requests we receive at once in_flight_requests = 0 did_lock = False @@ -304,7 +309,7 @@ async def callback(req): did_lock = True in_flight_requests += 1 - assert in_flight_requests <= 10 + assert in_flight_requests <= 2 await asyncio.sleep(0.1) await zboss_server.send(c.APS.DataReq.Rsp( @@ -317,17 +322,6 @@ async def callback(req): TxTime=1, DstAddrMode=req.DstAddrMode, )) - await asyncio.sleep(0.01) - await zboss_server.send( - c.APS.DataIndication.Ind( - ParamLength=21, PayloadLength=None, FrameFC=None, - SrcAddr=None, DstAddr=None, GrpAddr=None, DstEndpoint=1, - SrcEndpoint=1, ClusterId=6, ProfileId=260, - PacketCounter=None, SrcMACAddr=None, DstMACAddr=None, - LQI=None, - RSSI=None, KeySrcAndAttr=None, Payload=None, partial=True - ) - ) await asyncio.sleep(0) in_flight_requests -= 1 @@ -336,7 +330,9 @@ async def callback(req): asyncio.create_task(callback(req)) zboss_server.reply_to( - request=c.APS.DataReq.Req(partial=True), responses=[make_response] + request=c.APS.DataReq.Req( + partial=True), responses=[make_response] + ) # We create a whole bunch at once diff --git a/tests/conftest.py b/tests/conftest.py index c4dc62b..e5c567b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -543,7 +543,7 @@ def get_short_addr(self, request): NWKAddr=t.NWK(0x1234) # Example NWK address ) - @reply_to(c.APS.DataReq.Req(partial=True)) + @reply_to(c.APS.DataReq.Req(partial=True, DstEndpoint=0)) def on_zdo_request(self, req): # kwargs = deserialize_zdo_command(req.ClusterId, req.Payload) # handler_name = f"on_zdo_{zdo_t.ZDOCmd(req.ClusterId).name.lower()}" From 5e8b88e069fd6950b11cf948d4e411260c74b258 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 13 Jun 2024 14:02:16 +0400 Subject: [PATCH 28/57] add more application requests tests --- tests/application/test_requests.py | 800 ++++++----------------------- 1 file changed, 161 insertions(+), 639 deletions(-) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index be3d952..6fe6ce8 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -1,11 +1,9 @@ import asyncio - import pytest from unittest.mock import AsyncMock as CoroutineMock import zigpy.types as zigpy_t import zigpy.endpoint import zigpy.profiles -import zigpy.zdo.types as zdo_t from zigpy.exceptions import DeliveryError import zigpy_zboss.types as t @@ -13,8 +11,6 @@ import zigpy_zboss.commands as c from ..conftest import ( - zdo_request_matcher, - serialize_zdo_command, BaseZStackDevice ) @@ -356,642 +352,168 @@ async def callback(req): await app.shutdown() +@pytest.mark.asyncio +async def test_request_cancellation_shielding( + make_application, mocker, event_loop +): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + await app.startup(auto_form=False) + + device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + + ep = device.add_endpoint(1) + ep.status = zigpy.endpoint.Status.ZDO_INIT + ep.profile_id = 260 + ep.add_input_cluster(6) + + frame_control_byte = 0x18 + tsn = 0x01 + command_id = 0x01 + + payload = [frame_control_byte, tsn, command_id] + payload_length = len(payload) + + # The data confirm timeout must be shorter than the ARSP timeout + mocker.spy(app._api, "_unhandled_command") + + delayed_reply_sent = event_loop.create_future() + + def delayed_reply(req): + async def inner(): + await asyncio.sleep(0.5) + await zboss_server.send( + c.APS.DataIndication.Ind( + ParamLength=21, + PayloadLength=payload_length, + FrameFC=t.APSFrameFC(0x01), + SrcAddr=t.NWK(0xAABB), + DstAddr=t.NWK(0x1234), + GrpAddr=t.NWK(0x5678), + DstEndpoint=1, + SrcEndpoint=1, + ClusterId=6, + ProfileId=260, + PacketCounter=10, + SrcMACAddr=t.NWK(0xAABB), + DstMACAddr=t.NWK(0x1234), + LQI=255, + RSSI=-70, + KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(payload) + ) + ) + delayed_reply_sent.set_result(True) + + asyncio.create_task(inner()) + + data_req = zboss_server.reply_once_to( + c.APS.DataReq.Req( + TSN=1, ParamLength=21, DataLength=3, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + ProfileID=260, ClusterId=6, DstEndpoint=1, SrcEndpoint=1, Radius=0, + DstAddrMode=zigpy_t.AddrMode.NWK, + TxOptions=c.aps.TransmitOptions.NONE, + UseAlias=t.Bool.false, AliasSrcAddr=0x0000, AliasSeqNbr=0, + Payload=[1, 1, 1]), + responses=[ + c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=1, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=zigpy_t.AddrMode.NWK + ), + delayed_reply, + ], + ) + + with pytest.raises(asyncio.TimeoutError): + # Turn on the light + await device.request( + 260, + 6, + 1, + 1, + 1, + b'\x01\x01\x01', + expect_reply=True, + timeout=0.1, + ) + + await data_req + await delayed_reply_sent + + assert app._api._unhandled_command.call_count == 0 + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_send_security_and_packet_source_route(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + packet = zigpy_t.ZigbeePacket( + src=zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=app.state.node_info.nwk + ), + src_ep=0x9A, + dst=zigpy.types.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF + ), + dst_ep=0xBC, + tsn=0xDE, + profile_id=0x1234, + cluster_id=0x0006, + data=zigpy_t.SerializableBytes(b"test data"), + extended_timeout=False, + tx_options=( + zigpy_t.TransmitOptions.ACK | + zigpy_t.TransmitOptions.APS_Encryption + ), + source_route=[0xAABB, 0xCCDD], + ) + + data_req = zboss_server.reply_once_to( + request=c.APS.DataReq.Req( + TSN=222, ParamLength=21, DataLength=9, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:ee:ff"), + ProfileID=4660, ClusterId=6, DstEndpoint=188, SrcEndpoint=154, + Radius=0, DstAddrMode=zigpy_t.AddrMode.NWK, + TxOptions=( + c.aps.TransmitOptions.SECURITY_ENABLED | + c.aps.TransmitOptions.ACK_ENABLED + ), + UseAlias=t.Bool.false, AliasSrcAddr=0x0000, AliasSeqNbr=0, + Payload=[116, 101, 115, 116, 32, 100, 97, 116, 97]), + responses=[ + c.APS.DataReq.Rsp( + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=1, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=zigpy_t.AddrMode.NWK + ), + ], + ) + + await app.send_packet(packet) + req = await data_req + assert ( + c.aps.TransmitOptions.SECURITY_ENABLED + in c.aps.TransmitOptions(req.TxOptions) + ) + + await app.shutdown() + -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# async def test_nonstandard_profile(device, make_application): -# app, zboss_server = make_application(server_cls=device) -# await app.startup(auto_form=False) -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xFA9E) -# -# ep = device.add_endpoint(2) -# ep.status = zigpy.endpoint.Status.ZDO_INIT -# ep.profile_id = 0x9876 # non-standard profile -# ep.add_input_cluster(0x0006) -# -# # Respond to a light turn on request -# data_req = zboss_server.reply_once_to( -# request=c.AF.DataRequestExt.Req( -# DstAddrModeAddress=t.AddrModeAddress( -# mode=t.AddrMode.NWK, address=device.nwk -# ), -# DstEndpoint=2, -# SrcEndpoint=1, # we default to endpoint 1 for unknown profiles -# ClusterId=0x0006, -# partial=True, -# ), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# lambda req: c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS, -# Endpoint=2, -# TSN=req.TSN, -# ), -# lambda req: c.AF.IncomingMsg.Callback( -# GroupId=0x0000, -# ClusterId=0x0006, -# SrcAddr=device.nwk, -# SrcEndpoint=2, -# DstEndpoint=1, -# WasBroadcast=t.Bool(False), -# LQI=63, -# SecurityUse=t.Bool(False), -# TimeStamp=12345678, -# TSN=0, -# Data=b"\x08" + bytes([req.TSN]) + b"\x0B\x00\x00", -# MacSrcAddr=device.nwk, -# MsgResultRadius=29, -# ), -# ], -# ) -# -# await device.endpoints[2].on_off.off() -# -# await data_req -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# async def test_request_cancellation_shielding( -# device, make_application, mocker, event_loop -# ): -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# # The data confirm timeout must be shorter than the ARSP timeout -# mocker.spy(app._api, "_unhandled_command") -# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) -# -# delayed_reply_sent = event_loop.create_future() -# -# def delayed_reply(req): -# async def inner(): -# # Happens after DATA_CONFIRM_TIMEOUT expires but before ARSP_TIMEOUT -# await asyncio.sleep(0.5) -# zboss_server.send( -# c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS, Endpoint=1, TSN=req.TSN -# ) -# ) -# delayed_reply_sent.set_result(True) -# -# asyncio.create_task(inner()) -# -# data_req = zboss_server.reply_once_to( -# c.AF.DataRequestExt.Req(partial=True), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# delayed_reply, -# ], -# ) -# -# with pytest.raises(asyncio.TimeoutError): -# await app.request( -# device=device, -# profile=260, -# cluster=1, -# src_ep=1, -# dst_ep=1, -# sequence=1, -# data=b"\x00", -# ) -# -# await data_req -# await delayed_reply_sent -# -# assert app._api._unhandled_command.call_count == 0 -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_request_recovery_route_rediscovery_zdo(device, make_application, mocker): -# TSN = 1 -# -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# # The data confirm timeout must be shorter than the ARSP timeout -# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) -# -# # Fail the first time -# route_discovered = False -# -# def route_replier(req): -# nonlocal route_discovered -# -# if not route_discovered: -# return c.ZDO.ExtRouteChk.Rsp(Status=c.zdo.RoutingStatus.FAIL) -# else: -# return c.ZDO.ExtRouteChk.Rsp(Status=c.zdo.RoutingStatus.SUCCESS) -# -# def set_route_discovered(req): -# nonlocal route_discovered -# route_discovered = True -# -# return c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS) -# -# zboss_server.reply_to( -# request=c.ZDO.ExtRouteChk.Req(Dst=device.nwk, partial=True), -# responses=[route_replier], -# override=True, -# ) -# -# was_route_discovered = zboss_server.reply_once_to( -# request=c.ZDO.ExtRouteDisc.Req( -# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True -# ), -# responses=[set_route_discovered], -# ) -# -# zdo_req = zboss_server.reply_once_to( -# request=zdo_request_matcher( -# dst_addr=t.AddrModeAddress(t.AddrMode.NWK, device.nwk), -# command_id=zdo_t.ZDOCmd.Active_EP_req, -# TSN=TSN, -# zdo_NWKAddrOfInterest=device.nwk, -# ), -# responses=[ -# c.ZDO.ActiveEpRsp.Callback( -# Src=device.nwk, -# Status=t.ZDOStatus.SUCCESS, -# NWK=device.nwk, -# ActiveEndpoints=[], -# ), -# c.ZDO.MsgCbIncoming.Callback( -# Src=device.nwk, -# IsBroadcast=t.Bool.false, -# ClusterId=zdo_t.ZDOCmd.Active_EP_rsp, -# SecurityUse=0, -# TSN=TSN, -# MacDst=device.nwk, -# Data=serialize_zdo_command( -# command_id=zdo_t.ZDOCmd.Active_EP_rsp, -# Status=t.ZDOStatus.SUCCESS, -# NWKAddrOfInterest=device.nwk, -# ActiveEPList=[], -# ), -# ), -# ], -# ) -# -# await device.zdo.Active_EP_req(device.nwk) -# -# await was_route_discovered -# await zdo_req -# -# # 6 accounts for the loopback requests -# assert sum(c.value for c in app.state.counters["Retry_NONE"].values()) == 6 + 1 -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_request_recovery_route_rediscovery_af(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# # The data confirm timeout must be shorter than the ARSP timeout -# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) -# -# # Fail the first time -# route_discovered = False -# -# def data_confirm_replier(req): -# nonlocal route_discovered -# -# return c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS if route_discovered else t.Status.NWK_NO_ROUTE, -# Endpoint=1, -# TSN=1, -# ) -# -# def set_route_discovered(req): -# nonlocal route_discovered -# route_discovered = True -# -# return c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS) -# -# was_route_discovered = zboss_server.reply_once_to( -# c.ZDO.ExtRouteDisc.Req( -# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True -# ), -# responses=[set_route_discovered], -# ) -# -# zboss_server.reply_to( -# c.AF.DataRequestExt.Req(partial=True), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# # Ignore the source routing request as well -# zboss_server.reply_to( -# c.AF.DataRequestSrcRtg.Req(partial=True), -# responses=[ -# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# await app.request( -# device=device, -# profile=260, -# cluster=1, -# src_ep=1, -# dst_ep=1, -# sequence=1, -# data=b"\x00", -# ) -# -# await was_route_discovered -# assert ( -# sum(c.value for c in app.state.counters["Retry_RouteDiscovery"].values()) == 1 -# ) -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_request_recovery_use_ieee_addr(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# # The data confirm timeout must be shorter than the ARSP timeout -# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) -# -# was_ieee_addr_used = False -# -# def data_confirm_replier(req): -# nonlocal was_ieee_addr_used -# -# if req.DstAddrModeAddress.mode == t.AddrMode.IEEE: -# status = t.Status.SUCCESS -# was_ieee_addr_used = True -# else: -# status = t.Status.MAC_NO_ACK -# -# return c.AF.DataConfirm.Callback(Status=status, Endpoint=1, TSN=1) -# -# zboss_server.reply_once_to( -# c.ZDO.ExtRouteDisc.Req( -# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True -# ), -# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# zboss_server.reply_to( -# c.AF.DataRequestExt.Req(partial=True), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# # Ignore the source routing request as well -# zboss_server.reply_to( -# c.AF.DataRequestSrcRtg.Req(partial=True), -# responses=[ -# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), -# c.AF.DataConfirm.Callback(Status=t.Status.MAC_NO_ACK, Endpoint=1, TSN=1), -# ], -# ) -# -# await app.request( -# device=device, -# profile=260, -# cluster=1, -# src_ep=1, -# dst_ep=1, -# sequence=1, -# data=b"\x00", -# ) -# -# assert was_ieee_addr_used -# assert sum(c.value for c in app.state.counters["Retry_IEEEAddress"].values()) == 1 -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device_cls", FORMED_DEVICES) -# @pytest.mark.parametrize("fw_assoc_remove", [True, False]) -# @pytest.mark.parametrize("final_status", [t.Status.SUCCESS, t.Status.APS_NO_ACK]) -# async def test_request_recovery_assoc_remove( -# device_cls, fw_assoc_remove, final_status, make_application, mocker -# ): -# app, zboss_server = make_application(server_cls=device_cls) -# -# await app.startup(auto_form=False) -# -# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# mocker.patch("zigpy_zboss.zigbee.application.REQUEST_ERROR_RETRY_DELAY", new=0) -# -# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) -# -# assoc_device, _ = c.util.Device.deserialize(b"\xFF" * 100) -# assoc_device.shortAddr = device.nwk -# assoc_device.nodeRelation = c.util.NodeRelation.CHILD_FFD_RX_IDLE -# -# def data_confirm_replier(req): -# bad_assoc = assoc_device -# -# return c.AF.DataConfirm.Callback( -# Status=t.Status.MAC_TRANSACTION_EXPIRED if bad_assoc else final_status, -# Endpoint=1, -# TSN=1, -# ) -# -# zboss_server.reply_to( -# c.AF.DataRequestExt.Req(partial=True), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# zboss_server.reply_to( -# c.AF.DataRequestSrcRtg.Req(partial=True), -# responses=[ -# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# def assoc_get_with_addr(req): -# nonlocal assoc_device -# -# if assoc_device is None: -# dev, _ = c.util.Device.deserialize(b"\xFF" * 100) -# return c.UTIL.AssocGetWithAddress.Rsp(Device=dev) -# -# return c.UTIL.AssocGetWithAddress.Rsp(Device=assoc_device) -# -# did_assoc_get = zboss_server.reply_once_to( -# c.UTIL.AssocGetWithAddress.Req(IEEE=device.ieee, partial=True), -# responses=[assoc_get_with_addr], -# ) -# -# if not issubclass(device_cls, FormedLaunchpadCC26X2R1): -# fw_assoc_remove = False -# -# # Not all firmwares support Add/Remove -# if fw_assoc_remove: -# -# def assoc_remove(req): -# nonlocal assoc_device -# -# if assoc_device is None: -# return c.UTIL.AssocRemove.Rsp(Status=t.Status.FAILURE) -# -# assoc_device = None -# return c.UTIL.AssocRemove.Rsp(Status=t.Status.SUCCESS) -# -# did_assoc_remove = zboss_server.reply_once_to( -# c.UTIL.AssocRemove.Req(IEEE=device.ieee), -# responses=[assoc_remove], -# ) -# -# did_assoc_add = zboss_server.reply_once_to( -# c.UTIL.AssocAdd.Req( -# NWK=device.nwk, -# IEEE=device.ieee, -# NodeRelation=c.util.NodeRelation.CHILD_FFD_RX_IDLE, -# ), -# responses=[c.UTIL.AssocAdd.Rsp(Status=t.Status.SUCCESS)], -# ) -# else: -# did_assoc_remove = None -# did_assoc_add = None -# -# was_route_discovered = zboss_server.reply_to( -# c.ZDO.ExtRouteDisc.Req( -# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True -# ), -# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# req = app.request( -# device=device, -# profile=260, -# cluster=1, -# src_ep=1, -# dst_ep=1, -# sequence=1, -# data=b"\x00", -# ) -# -# if fw_assoc_remove and final_status == t.Status.SUCCESS: -# await req -# else: -# with pytest.raises(DeliveryError): -# await req -# -# if fw_assoc_remove: -# await did_assoc_remove -# -# if final_status != t.Status.SUCCESS: -# # The association is re-added on failure -# await did_assoc_add -# else: -# assert not did_assoc_add.done() -# elif issubclass(device_cls, FormedLaunchpadCC26X2R1): -# await did_assoc_get -# assert was_route_discovered.call_count >= 1 -# else: -# # Don't even attempt this with older firmwares -# assert not did_assoc_get.done() -# assert was_route_discovered.call_count == 0 -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# @pytest.mark.parametrize("succeed", [True, False]) -# @pytest.mark.parametrize("relays", [[0x1111, 0x2222, 0x3333], []]) -# async def test_request_recovery_manual_source_route( -# device, succeed, relays, make_application, mocker -# ): -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# mocker.patch("zigpy_zboss.zigbee.application.DATA_CONFIRM_TIMEOUT", new=0.1) -# mocker.patch("zigpy_zboss.zigbee.application.REQUEST_ERROR_RETRY_DELAY", new=0) -# -# app._api._config[conf.CONF_ZBOSS_CONFIG][conf.CONF_ARSP_TIMEOUT] = 1 -# -# device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xABCD) -# device.relays = relays -# -# def data_confirm_replier(req): -# if isinstance(req, c.AF.DataRequestExt.Req) or not succeed: -# return c.AF.DataConfirm.Callback( -# Status=t.Status.MAC_NO_ACK, -# Endpoint=1, -# TSN=1, -# ) -# else: -# return c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS, -# Endpoint=1, -# TSN=1, -# ) -# -# normal_data_request = zboss_server.reply_to( -# c.AF.DataRequestExt.Req(partial=True), -# responses=[ -# c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# source_routing_data_request = zboss_server.reply_to( -# c.AF.DataRequestSrcRtg.Req(partial=True), -# responses=[ -# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), -# data_confirm_replier, -# ], -# ) -# -# zboss_server.reply_to( -# c.ZDO.ExtRouteDisc.Req( -# Dst=device.nwk, Options=c.zdo.RouteDiscoveryOptions.UNICAST, partial=True -# ), -# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# req = app.request( -# device=device, -# profile=260, -# cluster=1, -# src_ep=1, -# dst_ep=1, -# sequence=1, -# data=b"\x00", -# ) -# -# if succeed: -# await req -# else: -# with pytest.raises(DeliveryError): -# await req -# -# # In either case only one source routing attempt is performed -# assert source_routing_data_request.call_count == 1 -# assert normal_data_request.call_count >= 1 -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -# async def test_route_discovery_concurrency(device, make_application): -# app, zboss_server = make_application(server_cls=device) -# -# await app.startup(auto_form=False) -# -# route_discovery1 = zboss_server.reply_to( -# c.ZDO.ExtRouteDisc.Req(Dst=0x1234, partial=True), -# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# route_discovery2 = zboss_server.reply_to( -# c.ZDO.ExtRouteDisc.Req(Dst=0x5678, partial=True), -# responses=[c.ZDO.ExtRouteDisc.Rsp(Status=t.Status.SUCCESS)], -# ) -# -# await asyncio.gather( -# app._discover_route(0x1234), -# app._discover_route(0x5678), -# app._discover_route(0x1234), -# app._discover_route(0x5678), -# app._discover_route(0x5678), -# app._discover_route(0x5678), -# app._discover_route(0x1234), -# ) -# -# assert route_discovery1.call_count == 1 -# assert route_discovery2.call_count == 1 -# -# await app._discover_route(0x5678) -# -# assert route_discovery1.call_count == 1 -# assert route_discovery2.call_count == 2 -# -# await app.shutdown() -# -# -# @pytest.mark.parametrize("device", FORMED_DEVICES) -# async def test_send_security_and_packet_source_route(device, make_application, mocker): -# app, zboss_server = make_application(server_cls=device) -# await app.startup(auto_form=False) -# -# packet = zigpy_t.ZigbeePacket( -# src=zigpy_t.AddrModeAddress( -# addr_mode=zigpy_t.AddrMode.NWK, address=app.state.node_info.nwk -# ), -# src_ep=0x9A, -# dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), -# dst_ep=0xBC, -# tsn=0xDE, -# profile_id=0x1234, -# cluster_id=0x0006, -# data=zigpy_t.SerializableBytes(b"test data"), -# extended_timeout=False, -# tx_options=( -# zigpy_t.TransmitOptions.ACK | zigpy_t.TransmitOptions.APS_Encryption -# ), -# source_route=[0xAABB, 0xCCDD], -# ) -# -# data_req = zboss_server.reply_once_to( -# request=c.AF.DataRequestSrcRtg.Req( -# DstAddr=packet.dst.address, -# DstEndpoint=packet.dst_ep, -# # SrcEndpoint=packet.src_ep, -# ClusterId=packet.cluster_id, -# TSN=packet.tsn, -# Data=packet.data.serialize(), -# SourceRoute=packet.source_route, -# partial=True, -# ), -# responses=[ -# c.AF.DataRequestSrcRtg.Rsp(Status=t.Status.SUCCESS), -# c.AF.DataConfirm.Callback( -# Status=t.Status.SUCCESS, -# Endpoint=packet.dst_ep, -# TSN=packet.tsn, -# ), -# ], -# ) -# -# await app.send_packet(packet) -# req = await data_req -# assert c.af.TransmitOptions.ENABLE_SECURITY in req.Options -# -# await app.shutdown() -# -# @pytest.mark.asyncio From 2809aad4f4d14de449b94d87012ba5010f7d5df9 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Wed, 19 Jun 2024 14:41:18 +0400 Subject: [PATCH 29/57] application startup tests --- tests/application/test_startup.py | 313 ++++++++++++++++++++++++++++++ tests/conftest.py | 173 ++++++++++++++++- 2 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 tests/application/test_startup.py diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py new file mode 100644 index 0000000..bff71ee --- /dev/null +++ b/tests/application/test_startup.py @@ -0,0 +1,313 @@ +import pytest +import voluptuous as vol +from unittest.mock import AsyncMock as CoroutineMock + +from zigpy.exceptions import NetworkNotFormed + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c +from zigpy_zboss.api import ZBOSS + +from ..conftest import ( + BaseZStackDevice, BaseZStackGenericDevice +) + + + +@pytest.mark.asyncio +async def test_info( + make_application, + caplog, +): + app, zboss_server = make_application( + server_cls=BaseZStackGenericDevice, active_sequence=True + ) + + pan_id = 0x5679 + ext_pan_id = t.EUI64.convert("00:11:22:33:44:55:66:77") + channel = 11 + channel_mask = 0x07fff800 + parent_address = t.NWK(0x5679) + coordinator_version = 1 + # Simulate responses for each request in load_network_info + zboss_server.reply_once_to( + request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), + responses=[c.NcpConfig.GetZigbeeRole.Rsp( + TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + DeviceRole=t.DeviceRole(1))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), + responses=[c.NcpConfig.GetZigbeeRole.Rsp( + TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + DeviceRole=t.DeviceRole(1))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetJoinStatus.Req(TSN=2), + responses=[c.NcpConfig.GetJoinStatus.Rsp( + TSN=2, StatusCat=t.StatusCategory(4), StatusCode=1, + Joined=1)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetShortAddr.Req(TSN=3), + responses=[c.NcpConfig.GetShortAddr.Rsp( + TSN=3, StatusCat=t.StatusCategory(4), StatusCode=1, + NWKAddr=t.NWK(0xAABB))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetLocalIEEE.Req(TSN=4, MacInterfaceNum=0), + responses=[c.NcpConfig.GetLocalIEEE.Rsp( + TSN=4, StatusCat=t.StatusCategory(4), StatusCode=1, + MacInterfaceNum=0, + IEEE=t.EUI64(range(8)))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetZigbeeRole.Req(TSN=5), + responses=[c.NcpConfig.GetZigbeeRole.Rsp( + TSN=5, StatusCat=t.StatusCategory(4), StatusCode=1, + DeviceRole=t.DeviceRole(2))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetExtendedPANID.Req(TSN=6), + responses=[c.NcpConfig.GetExtendedPANID.Rsp( + TSN=6, StatusCat=t.StatusCategory(4), StatusCode=1, + ExtendedPANID=ext_pan_id)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetShortPANID.Req(TSN=7), + responses=[c.NcpConfig.GetShortPANID.Rsp( + TSN=7, StatusCat=t.StatusCategory(4), StatusCode=1, + PANID=t.PanId(pan_id))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetCurrentChannel.Req(TSN=8), + responses=[c.NcpConfig.GetCurrentChannel.Rsp( + TSN=8, StatusCat=t.StatusCategory(4), StatusCode=1, + Channel=channel, Page=0)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetChannelMask.Req(TSN=9), + responses=[c.NcpConfig.GetChannelMask.Rsp( + TSN=9, StatusCat=t.StatusCategory(4), StatusCode=1, + ChannelList=[t.ChannelEntry(page=1, channel_mask=channel_mask)])] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetTrustCenterAddr.Req( + TSN=12 + ), + responses=[c.NcpConfig.GetTrustCenterAddr.Rsp( + TSN=12, + StatusCat=t.StatusCategory(1), + StatusCode=20, + TCIEEE=t.EUI64.convert("00:11:22:33:44:55:66:77") + # Example Trust Center IEEE address + )] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetRxOnWhenIdle.Req(TSN=13), + responses=[c.NcpConfig.GetRxOnWhenIdle.Rsp( + TSN=13, StatusCat=t.StatusCategory(4), StatusCode=1, + RxOnWhenIdle=1)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetEDTimeout.Req(TSN=14), + responses=[c.NcpConfig.GetEDTimeout.Rsp( + TSN=14, StatusCat=t.StatusCategory(4), StatusCode=1, + Timeout=t.TimeoutIndex(0x00))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetMaxChildren.Req(TSN=15), + responses=[c.NcpConfig.GetMaxChildren.Rsp( + TSN=15, StatusCat=t.StatusCategory(4), StatusCode=1, + ChildrenNbr=10)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetAuthenticationStatus.Req(TSN=16), + responses=[c.NcpConfig.GetAuthenticationStatus.Rsp( + TSN=16, StatusCat=t.StatusCategory(4), StatusCode=1, + Authenticated=True)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetParentAddr.Req(TSN=17), + responses=[c.NcpConfig.GetParentAddr.Rsp( + TSN=17, StatusCat=t.StatusCategory(4), StatusCode=1, + NWKParentAddr=parent_address)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetCoordinatorVersion.Req(TSN=18), + responses=[c.NcpConfig.GetCoordinatorVersion.Rsp( + TSN=18, StatusCat=t.StatusCategory(4), StatusCode=1, + CoordinatorVersion=coordinator_version)] + ) + + zboss_server.reply_once_to( + request=c.ZDO.PermitJoin.Req( + TSN=20, + DestNWK=t.NWK(0x0000), + PermitDuration=t.uint8_t(0), + TCSignificance=t.uint8_t(0x01), + ), + responses=[c.ZDO.PermitJoin.Rsp( + TSN=20, StatusCat=t.StatusCategory(4), StatusCode=1, + )] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.NCPModuleReset.Req( + TSN=21, Option=t.ResetOptions(0) + ), + responses=[c.NcpConfig.NCPModuleReset.Rsp( + TSN=21, StatusCat=t.StatusCategory(4), StatusCode=1 + )] + ) + + + await app.startup(auto_form=False) + + assert app.state.network_info.pan_id == 0x5679 + assert app.state.network_info.extended_pan_id == t.EUI64( + ext_pan_id.serialize()[::-1]) + assert app.state.network_info.channel == channel + assert app.state.network_info.channel_mask == channel_mask + assert app.state.network_info.network_key.seq == 1 + assert app.state.network_info.stack_specific[ + "parent_nwk" + ] == parent_address + assert app.state.network_info.stack_specific[ + "authenticated" + ] == 1 + assert app.state.network_info.stack_specific[ + "coordinator_version" + ] == coordinator_version + + # Anything to make sure it's set + assert app._device.node_desc.maximum_outgoing_transfer_size == 82 + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_endpoints(make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + endpoints = [] + zboss_server.register_indication_listener( + c.ZDO.PermitJoin.Req(partial=True), endpoints.append + ) + + await app.startup(auto_form=False) + + # We currently just register one endpoint + assert len(endpoints) == 1 + assert 1 in app._device.endpoints + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_not_configured(make_application): + app, zboss_server = make_application( + server_cls=BaseZStackGenericDevice, active_sequence=True + ) + + # Simulate responses for each request in load_network_info + zboss_server.reply_once_to( + request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), + responses=[c.NcpConfig.GetZigbeeRole.Rsp( + TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + DeviceRole=t.DeviceRole(1))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), + responses=[c.NcpConfig.GetZigbeeRole.Rsp( + TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + DeviceRole=t.DeviceRole(1))] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetJoinStatus.Req(TSN=2), + responses=[c.NcpConfig.GetJoinStatus.Rsp( + TSN=2, StatusCat=t.StatusCategory(4), StatusCode=1, + Joined=0)] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.NCPModuleReset.Req( + TSN=3, Option=t.ResetOptions(0) + ), + responses=[c.NcpConfig.NCPModuleReset.Rsp( + TSN=3, StatusCat=t.StatusCategory(4), StatusCode=1 + )] + ) + + # We cannot start the application if Z-Stack + # is not configured and without auto_form + with pytest.raises(NetworkNotFormed): + await app.startup(auto_form=False) + + +# @pytest.mark.asyncio +# async def test_reset(make_application, mocker): +# app, zboss_server = make_application(server_cls=BaseZStackDevice) +# +# # `_reset` should be called at least once +# # to put the radio into a consistent state +# mocker.spy(ZBOSS, "reset") +# assert ZBOSS.reset.call_count == 0 +# await app.startup() +# assert ZBOSS.reset.call_count >= 1 +# +# await app.shutdown() + + +@pytest.mark.asyncio +async def test_auto_form_unnecessary(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + mocker.patch.object(app, "form_network", new=CoroutineMock()) + + await app.startup(auto_form=True) + + assert app.form_network.call_count == 0 + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_auto_form_necessary(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + 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.state.network_info.channel != 0 + assert app.state.network_info.channel_mask != t.Channels.NO_CHANNELS + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_concurrency_auto_config(make_application): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.connect() + await app.start_network() + + assert app._concurrent_requests_semaphore.max_value == 8 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index e5c567b..64c585b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -368,6 +368,7 @@ def inner( server_cls, client_config=None, server_config=None, + active_sequence=False, **kwargs, ): default = config_for_port_path(FAKE_SERIAL_PORT) @@ -462,7 +463,10 @@ async def load_network_info(self, *, load_devices=False): app.device_initialized = Mock(wraps=app.device_initialized) app.listener_event = Mock(wraps=app.listener_event) - app.get_sequence = MagicMock(wraps=app.get_sequence, return_value=123) + if not active_sequence: + app.get_sequence = MagicMock( + wraps=app.get_sequence, return_value=123 + ) app.send_packet = AsyncMock(wraps=app.send_packet) app.write_network_info = AsyncMock(wraps=app.write_network_info) # app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) @@ -874,3 +878,170 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): ) return responses + +class BaseZStackGenericDevice(BaseServerZBOSS): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.active_endpoints = [] + self._nvram = {} + self._orig_nvram = {} + self.device_state = 0x00 + self.zdo_callbacks = set() + for name in dir(self): + func = getattr(self, name) + for req in getattr(func, "_reply_to", []): + self.reply_to(request=req, responses=[func]) + + def connection_lost(self, exc): + self.active_endpoints.clear() + return super().connection_lost(exc) + + @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) + def read_nvram(self, request): + status_code = 1 + if request.DatasetId == t.DatasetId.ZB_NVRAM_COMMON_DATA: + status_code = 0 + dataset = t.DSCommonData( + byte_count=100, + bitfield=1, + depth=1, + nwk_manager_addr=0x0000, + panid=0x1234, + network_addr=0x5678, + channel_mask=t.Channels(14), + aps_extended_panid=t.EUI64.convert("00:11:22:33:44:55:66:77"), + nwk_extended_panid=t.EUI64.convert("00:11:22:33:44:55:66:77"), + parent_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + tc_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + nwk_key=t.KeyData(b'\x01' * 16), + nwk_key_seq=0, + tc_standard_key=t.KeyData(b'\x02' * 16), + channel=15, + page=0, + mac_interface_table=t.MacInterfaceTable( + bitfield_0=0, + bitfield_1=1, + link_pwr_data_rate=250, + channel_in_use=11, + supported_channels=t.Channels(15) + ), + reserved=0 + ) + nvram_version = 3 + dataset_version = 1 + elif request.DatasetId == t.DatasetId.ZB_IB_COUNTERS: + status_code = 0 + dataset = t.DSIbCounters( + byte_count=8, + nib_counter=100, # Example counter value + aib_counter=50 # Example counter value + ) + nvram_version = 1 + dataset_version = 1 + elif request.DatasetId == t.DatasetId.ZB_NVRAM_ADDR_MAP: + status_code = 0 + dataset = t.DSNwkAddrMap( + header=t.NwkAddrMapHeader( + byte_count=100, + entry_count=2, + _align=0 + ), + items=[ + t.NwkAddrMapRecord( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + nwk_addr=0x1234, + index=1, + redirect_type=0, + redirect_ref=0, + _align=0 + ), + t.NwkAddrMapRecord( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:78"), + nwk_addr=0x5678, + index=2, + redirect_type=0, + redirect_ref=0, + _align=0 + ) + ] + ) + nvram_version = 2 + dataset_version = 1 + elif request.DatasetId == t.DatasetId.ZB_NVRAM_APS_SECURE_DATA: + status_code = 0 + dataset = t.DSApsSecureKeys( + header=10, + items=[ + t.ApsSecureEntry( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:77"), + key=t.KeyData(b'\x03' * 16), + _unknown_1=0 + ), + t.ApsSecureEntry( + ieee_addr=t.EUI64.convert("00:11:22:33:44:55:66:78"), + key=t.KeyData(b'\x04' * 16), + _unknown_1=0 + ) + ] + ) + nvram_version = 1 + dataset_version = 1 + else: + dataset = t.NVRAMDataset(b'') + nvram_version = 1 + dataset_version = 1 + + return c.NcpConfig.ReadNVRAM.Rsp( + TSN=request.TSN, + StatusCat=t.StatusCategory(1), + StatusCode=status_code, + NVRAMVersion=nvram_version, + DatasetId=t.DatasetId(request.DatasetId), + DatasetVersion=dataset_version, + Dataset=t.NVRAMDataset(dataset.serialize()) + ) + + def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): + if NWKAddrOfInterest != 0x0000: + return + + responses = [ + c.ZDO.NodeDescRsp.Callback( + Src=0x0000, + Status=t.ZDOStatus.SUCCESS, + NWK=0x0000, + NodeDescriptor=c.zdo.NullableNodeDescriptor( + byte1=0, + byte2=64, + mac_capability_flags=143, + manufacturer_code=0, + maximum_buffer_size=80, + maximum_incoming_transfer_size=160, + server_mask=1, # this differs + maximum_outgoing_transfer_size=160, + descriptor_capability_field=0, + ), + ), + ] + + if zdo_t.ZDOCmd.Node_Desc_rsp in self.zdo_callbacks: + responses.append( + c.ZDO.NodeDescReq.Callback( + Src=0x0000, + IsBroadcast=t.Bool.false, + ClusterId=zdo_t.ZDOCmd.Node_Desc_rsp, + SecurityUse=0, + TSN=req.TSN, + MacDst=0x0000, + Data=serialize_zdo_command( + command_id=zdo_t.ZDOCmd.Node_Desc_rsp, + Status=t.ZDOStatus.SUCCESS, + NWKAddrOfInterest=0x0000, + NodeDescriptor=zdo_t.NodeDescriptor( + **responses[0].NodeDescriptor.as_dict() + ), + ), + ) + ) + + return responses \ No newline at end of file From e025bfb8601c3852e0a20e3ec569214a5380b65c Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 20 Jun 2024 11:00:48 +0400 Subject: [PATCH 30/57] test zdo requests --- tests/application/test_startup.py | 25 ++++---- tests/application/test_zdo_requests.py | 81 ++++++++++++++++++++++++++ tests/conftest.py | 21 +++---- 3 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 tests/application/test_zdo_requests.py diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index bff71ee..a873618 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -263,18 +263,19 @@ async def test_not_configured(make_application): await app.startup(auto_form=False) -# @pytest.mark.asyncio -# async def test_reset(make_application, mocker): -# app, zboss_server = make_application(server_cls=BaseZStackDevice) -# -# # `_reset` should be called at least once -# # to put the radio into a consistent state -# mocker.spy(ZBOSS, "reset") -# assert ZBOSS.reset.call_count == 0 -# await app.startup() -# assert ZBOSS.reset.call_count >= 1 -# -# await app.shutdown() +@pytest.mark.asyncio +async def test_reset(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + # `_reset` should be called at least once + # to put the radio into a consistent state + mocker.spy(ZBOSS, "reset") + assert ZBOSS.reset.call_count == 0 + + await app.startup() + await app.shutdown() + + assert ZBOSS.reset.call_count >= 1 @pytest.mark.asyncio diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py new file mode 100644 index 0000000..f3056aa --- /dev/null +++ b/tests/application/test_zdo_requests.py @@ -0,0 +1,81 @@ +import asyncio + +import pytest +import zigpy.zdo.types as zdo_t +import zigpy.types as z_types + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c + +from ..conftest import ( + BaseZStackDevice +) + + +@pytest.mark.asyncio +async def test_mgmt_nwk_update_req( + make_application, mocker +): + mocker.patch( + "zigpy.application.CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S", 0.1 + ) + + app, zboss_server = make_application(server_cls=BaseZStackDevice) + + new_channel = 11 + + async def update_channel(req): + # Wait a bit before updating + await asyncio.sleep(0.5) + zboss_server.new_channel = new_channel + + yield + + zboss_server.reply_once_to( + request=c.APS.DataReq.Req( + TSN=123, ParamLength=21, DataLength=3, + ProfileID=260, ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_req, + DstEndpoint=0, partial=True), + responses=[c.APS.DataReq.Rsp( + TSN=123, + StatusCat=t.StatusCategory(1), + StatusCode=0, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=z_types.AddrMode.Group, + )], + ) + nwk_update_req = zboss_server.reply_once_to( + request=c.ZDO.MgmtNwkUpdate.Req( + TSN=123, + DstNWK=t.NWK(0x0000), + ScanChannelMask=t.Channels.from_channel_list([new_channel]), + ScanDuration=zdo_t.NwkUpdate.CHANNEL_CHANGE_REQ, + ScanCount=0, + MgrAddr=0x0000, + ), + responses=[ + c.ZDO.MgmtNwkUpdate.Rsp( + TSN=123, + StatusCat=t.StatusCategory(1), + StatusCode=0, + ScannedChannels=t.Channels.from_channel_list([new_channel]), + TotalTransmissions=1, + TransmissionFailures=0, + EnergyValues=c.zdo.EnergyValues(t.LVList([1])), + ), + update_channel, + ], + ) + + await app.startup(auto_form=False) + + await app.move_network_to_channel(new_channel=new_channel) + + await nwk_update_req + + assert app.state.network_info.channel == new_channel + + await app.shutdown() diff --git a/tests/conftest.py b/tests/conftest.py index 64c585b..6d95bee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -510,6 +510,7 @@ def __init__(self, *args, **kwargs): self.active_endpoints = [] self._nvram = {} self._orig_nvram = {} + self.new_channel = 0 self.device_state = 0x00 self.zdo_callbacks = set() for name in dir(self): @@ -549,19 +550,6 @@ def get_short_addr(self, request): @reply_to(c.APS.DataReq.Req(partial=True, DstEndpoint=0)) def on_zdo_request(self, req): - # kwargs = deserialize_zdo_command(req.ClusterId, req.Payload) - # handler_name = f"on_zdo_{zdo_t.ZDOCmd(req.ClusterId).name.lower()}" - # handler = getattr(self, handler_name, None) - # - # if handler is None: - # LOGGER.warning( - # "No ZDO handler %s, kwargs: %s", - # handler_name, kwargs - # ) - # return - # - # responses = handler(req=req, **kwargs) or [] - return c.APS.DataReq.Rsp( TSN=req.TSN, StatusCat=t.StatusCategory(1), @@ -620,12 +608,17 @@ def get_short_panid(self, request): @reply_to(c.NcpConfig.GetCurrentChannel.Req(partial=True)) def get_current_channel(self, request): + if self.new_channel != 0: + channel = self.new_channel + else: + channel = 1 + return c.NcpConfig.GetCurrentChannel.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), StatusCode=20, Page=0, - Channel=t.Channels(1) # Example channel + Channel=t.Channels(channel) ) @reply_to(c.NcpConfig.GetChannelMask.Req(partial=True)) From 887abcad715780bb3a73a896a06312fb079cd9b8 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:50:52 +0400 Subject: [PATCH 31/57] add pytest to CI --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++++++---- .pre-commit-config.yaml | 19 +++++----- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59ceacc..f3b0f63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,12 +3,20 @@ name: CI # yamllint disable-line rule:truthy on: push: - pull_request: ~ + branches: + - add-unit-tests + # pull_request: ~ env: - CACHE_VERSION: 1 + CODE_FOLDER: zigpy_zboss + CACHE_VERSION: 2 DEFAULT_PYTHON: 3.8 - PRE_COMMIT_HOME: ~/.cache/pre-commit + PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit + PYTHON_MATRIX: + description: 'Comma-separated list of Python versions e.g. "3.11","3.12"' + default: '"3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12"' + required: false + type: string jobs: # Separate job to pre-populate the base dependency cache @@ -18,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11"] + python-version: ${{ fromJSON(format('[{0}]', env.PYTHON_MATRIX)) }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 @@ -76,7 +84,7 @@ jobs: id: cache-precommit uses: actions/cache@v2 with: - path: ${{ env.PRE_COMMIT_HOME }} + path: ${{ env.PRE_COMMIT_CACHE_PATH }} key: | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: | @@ -86,6 +94,15 @@ jobs: run: | . venv/bin/activate pre-commit install-hooks + - name: Cache pre-commit environment + uses: actions/cache/save@v3 + with: + path: ${{ env.PRE_COMMIT_CACHE_PATH }} + key: ${{ steps.cache-precommit.outputs.cache-primary-key }} + - name: Lint and static analysis + run: | + . venv/bin/activate + pre-commit run --show-diff-on-failure --color=always --all-files lint-flake8: name: Check flake8 @@ -117,7 +134,7 @@ jobs: id: cache-precommit uses: actions/cache@v2 with: - path: ${{ env.PRE_COMMIT_HOME }} + path: ${{ env.PRE_COMMIT_CACHE_PATH }} key: | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed @@ -132,3 +149,51 @@ jobs: run: | . venv/bin/activate pre-commit run --hook-stage manual flake8 --all-files + + pytest: + runs-on: ubuntu-latest + needs: prepare-base + strategy: + matrix: + python-version: ${{ fromJSON(format('[{0}]', env.PYTHON_MATRIX )) }} + name: >- + Run tests Python ${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + id: python + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('setup.py', 'requirements_test.txt', 'requirements.txt', 'pyproject.toml', 'setup.cfg') }} + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Install Pytest Annotation plugin + run: | + . venv/bin/activate + # Ideally this should be part of our dependencies + # However this plugin is fairly new and doesn't run correctly + # on a non-GitHub environment. + pip install pytest-github-actions-annotate-failures + - name: Run pytest + run: | + . venv/bin/activate + pytest \ + -qq \ + --timeout=20 \ + --durations=10 \ + -o console_output_style=count \ + -p no:sugar \ + tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4546342..1829a4a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,15 @@ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: debug-statements + - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 5.0.4 hooks: - id: flake8 - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - - id: debug-statements - - id: no-commit-to-branch - args: - - --branch=dev - - --branch=main - - --branch=rc \ No newline at end of file + - id: isort From 580e9046acd1b535f88cd4b829fc930666d75735 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:57:55 +0400 Subject: [PATCH 32/57] update CI --- .github/workflows/ci.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3b0f63..d2a4e39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,8 @@ on: env: CODE_FOLDER: zigpy_zboss CACHE_VERSION: 2 - DEFAULT_PYTHON: 3.8 + DEFAULT_PYTHON: 3.10.8 PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit - PYTHON_MATRIX: - description: 'Comma-separated list of Python versions e.g. "3.11","3.12"' - default: '"3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12"' - required: false - type: string jobs: # Separate job to pre-populate the base dependency cache @@ -26,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ${{ fromJSON(format('[{0}]', env.PYTHON_MATRIX)) }} + python-version: ["3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12"] steps: - name: Check out code from GitHub uses: actions/checkout@v2 @@ -155,7 +150,7 @@ jobs: needs: prepare-base strategy: matrix: - python-version: ${{ fromJSON(format('[{0}]', env.PYTHON_MATRIX )) }} + python-version: ["3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12"] name: >- Run tests Python ${{ matrix.python-version }} steps: From 791e5b6683059e62dfe49aceec79cdfc7563c7f7 Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Thu, 20 Jun 2024 18:21:47 +0400 Subject: [PATCH 33/57] tests for zigpy callbacks --- tests/application/test_zdo_requests.py | 5 +- tests/application/test_zigpy_callbacks.py | 319 ++++++++++++++++++++++ 2 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 tests/application/test_zigpy_callbacks.py diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py index f3056aa..22173c4 100644 --- a/tests/application/test_zdo_requests.py +++ b/tests/application/test_zdo_requests.py @@ -23,10 +23,11 @@ async def test_mgmt_nwk_update_req( app, zboss_server = make_application(server_cls=BaseZStackDevice) new_channel = 11 + old_channel = 1 async def update_channel(req): # Wait a bit before updating - await asyncio.sleep(0.5) + await asyncio.sleep(0.1) zboss_server.new_channel = new_channel yield @@ -72,6 +73,8 @@ async def update_channel(req): await app.startup(auto_form=False) + assert app.state.network_info.channel == old_channel + await app.move_network_to_channel(new_channel=new_channel) await nwk_update_req diff --git a/tests/application/test_zigpy_callbacks.py b/tests/application/test_zigpy_callbacks.py new file mode 100644 index 0000000..56a12b7 --- /dev/null +++ b/tests/application/test_zigpy_callbacks.py @@ -0,0 +1,319 @@ +import asyncio +from unittest.mock import MagicMock + +import pytest +import zigpy.types as zigpy_t +import zigpy.zdo.types as zdo_t + +import zigpy_zboss.types as t +import zigpy_zboss.commands as c + +from ..conftest import BaseZStackDevice, serialize_zdo_command + + +@pytest.mark.asyncio +async def test_on_zdo_device_announce_nwk_change(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.spy(app, "handle_join") + mocker.patch.object(app, "handle_message") + + device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xFA9E) + new_nwk = device.nwk + 1 + + payload = bytearray(serialize_zdo_command( + command_id=zdo_t.ZDOCmd.Device_annce, + NWKAddr=new_nwk, + IEEEAddr=device.ieee, + Capability=t.MACCapability.DeviceType, + Status=t.DeviceUpdateStatus.tc_rejoin, + )) + payload_length = len(payload) + + # Assume its NWK changed and we're just finding out + await zboss_server.send( + c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=payload_length, + FrameFC=t.APSFrameFC(0x01), + SrcAddr=t.NWK(0x0001), DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x0000), DstEndpoint=1, + SrcEndpoint=1, ClusterId=zdo_t.ZDOCmd.Device_annce, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0x0000), + LQI=255, RSSI=-70, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(payload) + ) + ) + + await zboss_server.send( + c.ZDO.DevAnnceInd.Ind( + NWK=new_nwk, + IEEE=device.ieee, + MacCap=1, + ) + ) + + await asyncio.sleep(0.1) + + app.handle_join.assert_called_once_with( + nwk=new_nwk, ieee=device.ieee, parent_nwk=None + ) + + # The device's NWK has been updated + assert device.nwk == new_nwk + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_on_zdo_device_leave_callback(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.patch.object(app, "handle_leave") + + nwk = 0xAABB + ieee = t.EUI64(range(8)) + + await zboss_server.send( + c.NWK.NwkLeaveInd.Ind( + IEEE=ieee, Rejoin=0 + ) + ) + app.handle_leave.assert_called_once_with(nwk=nwk, ieee=ieee) + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_on_af_message_callback(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.patch.object(app, "packet_received") + device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) + + af_message = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=len(b"test"), + FrameFC=t.APSFrameFC(0x01), + SrcAddr=device.nwk, DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x0000), DstEndpoint=1, + SrcEndpoint=4, ClusterId=2, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0x0000), + LQI=19, RSSI=0, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(b"test") + ) + + # Normal message + await zboss_server.send(af_message) + await asyncio.sleep(0.1) + + + assert app.packet_received.call_count == 1 + _call = app.packet_received.call_args[0][0] + assert _call.src == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, + address=device.nwk, + ) + assert _call.src_ep == 4 + assert _call.dst == zigpy_t.AddrModeAddress( + zigpy_t.AddrMode.NWK, app.state.node_info.nwk + ) + assert _call.dst_ep == 1 + assert _call.cluster_id == 2 + assert _call.data.serialize() == b"test" + assert _call.lqi == 19 + assert _call.rssi == 0 + assert _call.profile_id == 260 + + app.packet_received.reset_mock() + + zll_message = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=len(b"test"), + FrameFC=t.APSFrameFC(0x01), + SrcAddr=device.nwk, DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x0000), DstEndpoint=2, + SrcEndpoint=4, ClusterId=2, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0x0000), + LQI=19, RSSI=0, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(b"test") + ) + + # ZLL message + await zboss_server.send(zll_message) + await asyncio.sleep(0.1) + + assert app.packet_received.call_count == 1 + _call = app.packet_received.call_args[0][0] + assert _call.src == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=device.nwk + ) + assert _call.src_ep == 4 + assert _call.dst == zigpy_t.AddrModeAddress( + zigpy_t.AddrMode.NWK, app.state.node_info.nwk + ) + assert _call.dst_ep == 2 + assert _call.cluster_id == 2 + assert _call.data.serialize() == b"test" + assert _call.lqi == 19 + assert _call.rssi == 0 + assert _call.profile_id == 260 + + app.packet_received.reset_mock() + + unknown_message = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=len(b"test"), + FrameFC=t.APSFrameFC(0x01), + SrcAddr=device.nwk, DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x0000), DstEndpoint=3, + SrcEndpoint=4, ClusterId=2, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0x0000), + LQI=19, RSSI=0, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(b"test") + ) + + # Message on an unknown endpoint (is this possible?) + await zboss_server.send(unknown_message) + await asyncio.sleep(0.1) + + assert app.packet_received.call_count == 1 + _call = app.packet_received.call_args[0][0] + assert _call.src == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=device.nwk + ) + assert _call.src_ep == 4 + assert _call.dst == zigpy_t.AddrModeAddress( + zigpy_t.AddrMode.NWK, app.state.node_info.nwk + ) + assert _call.dst_ep == 3 + assert _call.cluster_id == 2 + assert _call.data.serialize() == b"test" + assert _call.lqi == 19 + assert _call.rssi == 0 + assert _call.profile_id == 260 + + app.packet_received.reset_mock() + + +@pytest.mark.asyncio +async def test_receive_zdo_broadcast(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.patch.object(app, "packet_received") + + zdo_callback = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=len(b"bogus"), + FrameFC=t.APSFrameFC.Broadcast, + SrcAddr=t.NWK(0x35D9), DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x0000), DstEndpoint=0, + SrcEndpoint=0, ClusterId=19, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0xFFFF), + LQI=19, RSSI=0, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(b"bogus") + ) + await zboss_server.send(zdo_callback) + await asyncio.sleep(0.1) + + assert app.packet_received.call_count == 1 + packet = app.packet_received.mock_calls[0].args[0] + assert packet.src == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=0x35D9 + ) + assert packet.src_ep == 0x00 + assert packet.dst == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.Broadcast, + address=zigpy_t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, + ) + assert packet.dst_ep == 0x00 + assert packet.cluster_id == zdo_callback.ClusterId + assert packet.data.serialize() == zdo_callback.Payload.serialize() + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_receive_af_broadcast(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.patch.object(app, "packet_received") + + payload = b"\x11\xA6\x00\x74\xB5\x7C\x00\x02\x5F" + + af_callback = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=len(payload), + FrameFC=t.APSFrameFC.Broadcast, + SrcAddr=t.NWK(0x1234), DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x0000), DstEndpoint=2, + SrcEndpoint=254, ClusterId=4096, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0xFFFF), + LQI=19, RSSI=0, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(payload) + ) + await zboss_server.send(af_callback) + await asyncio.sleep(0.1) + + assert app.packet_received.call_count == 1 + packet = app.packet_received.mock_calls[0].args[0] + assert packet.src == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, + address=0x1234, + ) + assert packet.src_ep == af_callback.SrcEndpoint + assert packet.dst == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.Broadcast, + address=zigpy_t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, + ) + assert packet.dst_ep == af_callback.DstEndpoint + assert packet.cluster_id == af_callback.ClusterId + assert packet.lqi == af_callback.LQI + assert packet.data.serialize() == af_callback.Payload.serialize() + + await app.shutdown() + + +@pytest.mark.asyncio +async def test_receive_af_group(make_application, mocker): + app, zboss_server = make_application(server_cls=BaseZStackDevice) + await app.startup(auto_form=False) + + mocker.patch.object(app, "packet_received") + + payload = b"\x11\xA6\x00\x74\xB5\x7C\x00\x02\x5F" + + af_callback = c.APS.DataIndication.Ind( + ParamLength=21, PayloadLength=len(payload), + FrameFC=t.APSFrameFC.Group, + SrcAddr=t.NWK(0x1234), DstAddr=t.NWK(0x0000), + GrpAddr=t.NWK(0x1234) , DstEndpoint=0, + SrcEndpoint=254, ClusterId=4096, ProfileId=260, + PacketCounter=10, SrcMACAddr=t.NWK(0x0000), + DstMACAddr=t.NWK(0xFFFF), + LQI=19, RSSI=0, KeySrcAndAttr=t.ApsAttributes(0x01), + Payload=t.Payload(payload) + ) + await zboss_server.send(af_callback) + await asyncio.sleep(0.1) + + assert app.packet_received.call_count == 1 + packet = app.packet_received.mock_calls[0].args[0] + assert packet.src == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, + address=0x1234, + ) + assert packet.src_ep == af_callback.SrcEndpoint + assert packet.dst == zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.Group, address=0x1234 + ) + assert packet.cluster_id == af_callback.ClusterId + assert packet.lqi == af_callback.LQI + assert packet.data.serialize() == af_callback.Payload.serialize() + + await app.shutdown() \ No newline at end of file From 200ad37abfd7b1222ed0adddd5980c3edc7c879a Mon Sep 17 00:00:00 2001 From: Hamid Abubakr Date: Fri, 21 Jun 2024 09:50:16 +0400 Subject: [PATCH 34/57] clean up --- tests/application/test_join.py | 2 ++ tests/application/test_requests.py | 40 ++++++++++++++--------- tests/application/test_startup.py | 37 ++++++++++----------- tests/application/test_zdo_requests.py | 26 +++++++-------- tests/application/test_zigpy_callbacks.py | 24 +++++++------- tests/conftest.py | 38 ++------------------- 6 files changed, 69 insertions(+), 98 deletions(-) diff --git a/tests/application/test_join.py b/tests/application/test_join.py index bca26e2..5ad80c6 100644 --- a/tests/application/test_join.py +++ b/tests/application/test_join.py @@ -37,6 +37,8 @@ async def test_permit_join(mocker, make_application): await app.startup(auto_form=False) await app.permit(time_s=10) + await asyncio.sleep(0.1) + assert permit_join_coordinator.done() await app.shutdown() diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 6fe6ce8..587992b 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -4,7 +4,6 @@ import zigpy.types as zigpy_t import zigpy.endpoint import zigpy.profiles -from zigpy.exceptions import DeliveryError import zigpy_zboss.types as t import zigpy_zboss.config as conf @@ -133,8 +132,10 @@ async def test_zigpy_request(make_application): @pytest.mark.parametrize( "addr", [ - zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.IEEE, address=t.EUI64(range(8))), - zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.NWK, address=t.NWK(0xAABB)), + zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.IEEE, + address=t.EUI64(range(8))), + zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.NWK, + address=t.NWK(0xAABB)), ], ) async def test_request_addr_mode(addr, make_application, mocker): @@ -162,6 +163,7 @@ async def test_request_addr_mode(addr, make_application, mocker): await app.shutdown() + @pytest.mark.asyncio async def test_mrequest(make_application, mocker): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -173,15 +175,17 @@ async def test_mrequest(make_application, mocker): assert app.send_packet.call_count == 1 assert ( - app.send_packet.mock_calls[0].args[0].dst - == zigpy.types.AddrModeAddress( - addr_mode=zigpy.types.AddrMode.Group, address=0x1234 - ) + app.send_packet.mock_calls[0].args[0].dst + == zigpy.types.AddrModeAddress( + addr_mode=zigpy.types.AddrMode.Group, address=0x1234 + ) ) - assert app.send_packet.mock_calls[0].args[0].data.serialize() == b"\x01\x01\x01" + assert app.send_packet.mock_calls[0].args[ + 0].data.serialize() == b"\x01\x01\x01" await app.shutdown() + @pytest.mark.asyncio async def test_mrequest_doesnt_block(make_application, event_loop): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -218,6 +222,7 @@ async def test_mrequest_doesnt_block(make_application, event_loop): ) request_sent = event_loop.create_future() + async def on_request_sent(): await zboss_server.send(data_confirm_rsp) @@ -233,6 +238,7 @@ async def on_request_sent(): await app.shutdown() + @pytest.mark.asyncio async def test_broadcast(make_application, mocker): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -276,6 +282,7 @@ async def test_broadcast(make_application, mocker): await app.shutdown() + @pytest.mark.asyncio async def test_request_concurrency(make_application, mocker): app, zboss_server = make_application( @@ -352,9 +359,10 @@ async def callback(req): await app.shutdown() + @pytest.mark.asyncio async def test_request_cancellation_shielding( - make_application, mocker, event_loop + make_application, mocker, event_loop ): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -472,8 +480,8 @@ async def test_send_security_and_packet_source_route(make_application, mocker): data=zigpy_t.SerializableBytes(b"test data"), extended_timeout=False, tx_options=( - zigpy_t.TransmitOptions.ACK | - zigpy_t.TransmitOptions.APS_Encryption + zigpy_t.TransmitOptions.ACK | + zigpy_t.TransmitOptions.APS_Encryption ), source_route=[0xAABB, 0xCCDD], ) @@ -514,8 +522,6 @@ async def test_send_security_and_packet_source_route(make_application, mocker): await app.shutdown() - - @pytest.mark.asyncio async def test_send_packet_failure_disconnected(make_application, mocker): app, zboss_server = make_application(server_cls=BaseZStackDevice) @@ -524,9 +530,11 @@ async def test_send_packet_failure_disconnected(make_application, mocker): app._api = None packet = zigpy_t.ZigbeePacket( - src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), + src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, + address=0x0000), src_ep=0x9A, - dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), + dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, + address=0xEEFF), dst_ep=0xBC, tsn=0xDE, profile_id=0x1234, @@ -539,4 +547,4 @@ async def test_send_packet_failure_disconnected(make_application, mocker): assert "Coordinator is disconnected" in str(excinfo.value) - await app.shutdown() \ No newline at end of file + await app.shutdown() diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index a873618..749e6ed 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -1,5 +1,4 @@ import pytest -import voluptuous as vol from unittest.mock import AsyncMock as CoroutineMock from zigpy.exceptions import NetworkNotFormed @@ -13,11 +12,10 @@ ) - @pytest.mark.asyncio async def test_info( - make_application, - caplog, + make_application, + caplog, ): app, zboss_server = make_application( server_cls=BaseZStackGenericDevice, active_sequence=True @@ -158,14 +156,14 @@ async def test_info( zboss_server.reply_once_to( request=c.ZDO.PermitJoin.Req( - TSN=20, - DestNWK=t.NWK(0x0000), - PermitDuration=t.uint8_t(0), - TCSignificance=t.uint8_t(0x01), - ), + TSN=20, + DestNWK=t.NWK(0x0000), + PermitDuration=t.uint8_t(0), + TCSignificance=t.uint8_t(0x01), + ), responses=[c.ZDO.PermitJoin.Rsp( - TSN=20, StatusCat=t.StatusCategory(4), StatusCode=1, - )] + TSN=20, StatusCat=t.StatusCategory(4), StatusCode=1, + )] ) zboss_server.reply_once_to( @@ -177,24 +175,23 @@ async def test_info( )] ) - await app.startup(auto_form=False) assert app.state.network_info.pan_id == 0x5679 assert app.state.network_info.extended_pan_id == t.EUI64( - ext_pan_id.serialize()[::-1]) + ext_pan_id.serialize()[::-1]) assert app.state.network_info.channel == channel assert app.state.network_info.channel_mask == channel_mask assert app.state.network_info.network_key.seq == 1 assert app.state.network_info.stack_specific[ - "parent_nwk" - ] == parent_address + "parent_nwk" + ] == parent_address assert app.state.network_info.stack_specific[ - "authenticated" - ] == 1 + "authenticated" + ] == 1 assert app.state.network_info.stack_specific[ - "coordinator_version" - ] == coordinator_version + "coordinator_version" + ] == coordinator_version # Anything to make sure it's set assert app._device.node_desc.maximum_outgoing_transfer_size == 82 @@ -311,4 +308,4 @@ async def test_concurrency_auto_config(make_application): await app.connect() await app.start_network() - assert app._concurrent_requests_semaphore.max_value == 8 \ No newline at end of file + assert app._concurrent_requests_semaphore.max_value == 8 diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py index 22173c4..73ac4bb 100644 --- a/tests/application/test_zdo_requests.py +++ b/tests/application/test_zdo_requests.py @@ -14,7 +14,7 @@ @pytest.mark.asyncio async def test_mgmt_nwk_update_req( - make_application, mocker + make_application, mocker ): mocker.patch( "zigpy.application.CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S", 0.1 @@ -34,19 +34,19 @@ async def update_channel(req): zboss_server.reply_once_to( request=c.APS.DataReq.Req( - TSN=123, ParamLength=21, DataLength=3, - ProfileID=260, ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_req, - DstEndpoint=0, partial=True), + TSN=123, ParamLength=21, DataLength=3, + ProfileID=260, ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_req, + DstEndpoint=0, partial=True), responses=[c.APS.DataReq.Rsp( - TSN=123, - StatusCat=t.StatusCategory(1), - StatusCode=0, - DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), - DstEndpoint=1, - SrcEndpoint=1, - TxTime=1, - DstAddrMode=z_types.AddrMode.Group, - )], + TSN=123, + StatusCat=t.StatusCategory(1), + StatusCode=0, + DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), + DstEndpoint=1, + SrcEndpoint=1, + TxTime=1, + DstAddrMode=z_types.AddrMode.Group, + )], ) nwk_update_req = zboss_server.reply_once_to( request=c.ZDO.MgmtNwkUpdate.Req( diff --git a/tests/application/test_zigpy_callbacks.py b/tests/application/test_zigpy_callbacks.py index 56a12b7..0733b39 100644 --- a/tests/application/test_zigpy_callbacks.py +++ b/tests/application/test_zigpy_callbacks.py @@ -1,5 +1,4 @@ import asyncio -from unittest.mock import MagicMock import pytest import zigpy.types as zigpy_t @@ -23,12 +22,12 @@ async def test_on_zdo_device_announce_nwk_change(make_application, mocker): new_nwk = device.nwk + 1 payload = bytearray(serialize_zdo_command( - command_id=zdo_t.ZDOCmd.Device_annce, - NWKAddr=new_nwk, - IEEEAddr=device.ieee, - Capability=t.MACCapability.DeviceType, - Status=t.DeviceUpdateStatus.tc_rejoin, - )) + command_id=zdo_t.ZDOCmd.Device_annce, + NWKAddr=new_nwk, + IEEEAddr=device.ieee, + Capability=t.MACCapability.DeviceType, + Status=t.DeviceUpdateStatus.tc_rejoin, + )) payload_length = len(payload) # Assume its NWK changed and we're just finding out @@ -110,13 +109,12 @@ async def test_on_af_message_callback(make_application, mocker): await zboss_server.send(af_message) await asyncio.sleep(0.1) - assert app.packet_received.call_count == 1 _call = app.packet_received.call_args[0][0] assert _call.src == zigpy_t.AddrModeAddress( - addr_mode=zigpy_t.AddrMode.NWK, - address=device.nwk, - ) + addr_mode=zigpy_t.AddrMode.NWK, + address=device.nwk, + ) assert _call.src_ep == 4 assert _call.dst == zigpy_t.AddrModeAddress( zigpy_t.AddrMode.NWK, app.state.node_info.nwk @@ -292,7 +290,7 @@ async def test_receive_af_group(make_application, mocker): ParamLength=21, PayloadLength=len(payload), FrameFC=t.APSFrameFC.Group, SrcAddr=t.NWK(0x1234), DstAddr=t.NWK(0x0000), - GrpAddr=t.NWK(0x1234) , DstEndpoint=0, + GrpAddr=t.NWK(0x1234), DstEndpoint=0, SrcEndpoint=254, ClusterId=4096, ProfileId=260, PacketCounter=10, SrcMACAddr=t.NWK(0x0000), DstMACAddr=t.NWK(0xFFFF), @@ -316,4 +314,4 @@ async def test_receive_af_group(make_application, mocker): assert packet.lqi == af_callback.LQI assert packet.data.serialize() == af_callback.Payload.serialize() - await app.shutdown() \ No newline at end of file + await app.shutdown() diff --git a/tests/conftest.py b/tests/conftest.py index 6d95bee..c83cd66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -422,44 +422,12 @@ async def start_network(self): ] ) - async def permit(self, dev): - pass - async def energy_scan(self, channels, duration_exp, count): return {self.state.network_info.channel: 0x1234} - async def force_remove(self, dev): - pass - - async def add_endpoint(self, descriptor): - pass - - async def permit_ncp(self, time_s=60): - pass - - async def permit_with_link_key(self, node, link_key, time_s=60): - pass - - async def reset_network_info(self): - pass - - async def write_network_info(self, *, network_info, node_info): - pass - - async def load_network_info(self, *, load_devices=False): - self.state.network_info.channel = 15 - app.add_initialized_device = add_initialized_device.__get__(app) app.start_network = start_network.__get__(app) - # app.permit = permit.__get__(app) app.energy_scan = energy_scan.__get__(app) - # app.force_remove = force_remove.__get__(app) - # app.add_endpoint = add_endpoint.__get__(app) - # app.permit_ncp = permit_ncp.__get__(app) - # app.permit_with_link_key = permit_with_link_key.__get__(app) - # app.reset_network_info = reset_network_info.__get__(app) - # app.write_network_info = write_network_info.__get__(app) - # app.load_network_info = load_network_info.__get__(app) app.device_initialized = Mock(wraps=app.device_initialized) app.listener_event = Mock(wraps=app.listener_event) @@ -469,7 +437,6 @@ async def load_network_info(self, *, load_devices=False): ) app.send_packet = AsyncMock(wraps=app.send_packet) app.write_network_info = AsyncMock(wraps=app.write_network_info) - # app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) server = make_zboss_server( server_cls=server_cls, config=server_config, **kwargs @@ -487,10 +454,8 @@ def zdo_request_matcher( kwargs = {k: v for k, v in kwargs.items() if not k.startswith("zdo_")} kwargs.setdefault("DstEndpoint", 0x00) - # kwargs.setdefault("DstPanId", 0x0000) kwargs.setdefault("SrcEndpoint", 0x00) kwargs.setdefault("Radius", None) - # kwargs.setdefault("Options", None) return c.APS.DataReq.Req( DstAddr=t.EUI64.convert("00124b0001ab89cd"), @@ -872,6 +837,7 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): return responses + class BaseZStackGenericDevice(BaseServerZBOSS): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1037,4 +1003,4 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): ) ) - return responses \ No newline at end of file + return responses From a940d1d8fb3d01f12bf15e722151a2f6c9f1b095 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:30:21 +0400 Subject: [PATCH 35/57] run isort for import cleaning --- tests/api/test_connect.py | 3 ++- tests/api/test_listeners.py | 4 ++-- tests/api/test_request.py | 9 ++++----- tests/api/test_response.py | 4 ++-- tests/application/test_connect.py | 5 ++--- tests/application/test_join.py | 11 ++++------- tests/application/test_requests.py | 12 +++++------- tests/application/test_startup.py | 9 ++++----- tests/application/test_zdo_requests.py | 8 +++----- tests/conftest.py | 17 ++++++++--------- tests/test_commands.py | 3 +-- tests/test_frame.py | 7 +++---- tests/test_nvids.py | 10 +++++----- tests/test_uart.py | 4 ++-- tests/test_utils.py | 2 +- zigpy_zboss/api.py | 20 ++++++++++---------- zigpy_zboss/commands/__init__.py | 6 +++--- zigpy_zboss/commands/aps.py | 1 + zigpy_zboss/commands/zdo.py | 2 +- zigpy_zboss/config.py | 26 ++++++++------------------ zigpy_zboss/debug.py | 5 +++-- zigpy_zboss/frames.py | 4 +--- zigpy_zboss/nvram.py | 2 +- zigpy_zboss/tools/factory_reset_ncp.py | 6 +++--- zigpy_zboss/tools/get_ncp_version.py | 4 ++-- zigpy_zboss/types/__init__.py | 4 ++-- zigpy_zboss/types/basic.py | 18 +++++------------- zigpy_zboss/types/commands.py | 7 +++---- zigpy_zboss/types/cstruct.py | 4 ++-- zigpy_zboss/types/named.py | 21 +++++---------------- zigpy_zboss/types/nvids.py | 3 +++ zigpy_zboss/types/structs.py | 1 + zigpy_zboss/uart.py | 10 ++++++---- zigpy_zboss/utils.py | 5 +++-- zigpy_zboss/zigbee/application.py | 24 +++++++++++++----------- zigpy_zboss/zigbee/device.py | 11 ++++++----- 36 files changed, 130 insertions(+), 162 deletions(-) diff --git a/tests/api/test_connect.py b/tests/api/test_connect.py index ffc6ee6..4bbfe59 100644 --- a/tests/api/test_connect.py +++ b/tests/api/test_connect.py @@ -1,9 +1,10 @@ """Test cases for zigpy-zboss API connect/close methods.""" import pytest -from ..conftest import config_for_port_path, BaseServerZBOSS from zigpy_zboss.api import ZBOSS +from ..conftest import BaseServerZBOSS, config_for_port_path + @pytest.mark.asyncio async def test_connect_no_test(make_zboss_server): diff --git a/tests/api/test_listeners.py b/tests/api/test_listeners.py index 18f1af5..7edf73c 100644 --- a/tests/api/test_listeners.py +++ b/tests/api/test_listeners.py @@ -3,9 +3,9 @@ import pytest -import zigpy_zboss.types as t import zigpy_zboss.commands as c -from zigpy_zboss.api import OneShotResponseListener, IndicationListener +import zigpy_zboss.types as t +from zigpy_zboss.api import IndicationListener, OneShotResponseListener @pytest.mark.asyncio diff --git a/tests/api/test_request.py b/tests/api/test_request.py index 4d2c756..a908b2b 100644 --- a/tests/api/test_request.py +++ b/tests/api/test_request.py @@ -1,14 +1,13 @@ import asyncio import logging -import pytest import async_timeout +import pytest -import zigpy_zboss.types as t import zigpy_zboss.commands as c -from zigpy_zboss.frames import ( - Frame, HLPacket, ZBNCP_LL_BODY_SIZE_MAX, LLHeader -) +import zigpy_zboss.types as t +from zigpy_zboss.frames import (ZBNCP_LL_BODY_SIZE_MAX, Frame, HLPacket, + LLHeader) @pytest.mark.asyncio diff --git a/tests/api/test_response.py b/tests/api/test_response.py index 1cdf8aa..a2a33b5 100644 --- a/tests/api/test_response.py +++ b/tests/api/test_response.py @@ -1,10 +1,10 @@ import asyncio -import pytest import async_timeout +import pytest -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t from zigpy_zboss.utils import deduplicate_commands diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index 5460458..59e6864 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -3,13 +3,12 @@ import pytest +import zigpy_zboss.commands as c import zigpy_zboss.config as conf +import zigpy_zboss.types as t from zigpy_zboss.uart import connect as uart_connect from zigpy_zboss.zigbee.application import ControllerApplication -import zigpy_zboss.types as t -import zigpy_zboss.commands as c - from ..conftest import BaseServerZBOSS, BaseZStackDevice diff --git a/tests/application/test_join.py b/tests/application/test_join.py index 5ad80c6..e94ff2f 100644 --- a/tests/application/test_join.py +++ b/tests/application/test_join.py @@ -1,17 +1,14 @@ import asyncio import pytest -import zigpy.util -import zigpy.types import zigpy.device +import zigpy.types +import zigpy.util -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t -from ..conftest import ( - BaseZStackDevice, - -) +from ..conftest import BaseZStackDevice @pytest.mark.asyncio diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 587992b..909b6e7 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -1,17 +1,15 @@ import asyncio -import pytest from unittest.mock import AsyncMock as CoroutineMock -import zigpy.types as zigpy_t + +import pytest import zigpy.endpoint import zigpy.profiles -import zigpy_zboss.types as t -import zigpy_zboss.config as conf import zigpy_zboss.commands as c +import zigpy_zboss.config as conf +import zigpy_zboss.types as t -from ..conftest import ( - BaseZStackDevice -) +from ..conftest import BaseZStackDevice @pytest.mark.asyncio diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index 749e6ed..21e3efa 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -1,16 +1,15 @@ import pytest from unittest.mock import AsyncMock as CoroutineMock +import pytest +import voluptuous as vol from zigpy.exceptions import NetworkNotFormed -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t from zigpy_zboss.api import ZBOSS -from ..conftest import ( - BaseZStackDevice, BaseZStackGenericDevice -) - +from ..conftest import BaseZStackDevice, BaseZStackGenericDevice @pytest.mark.asyncio async def test_info( diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py index 73ac4bb..32691ee 100644 --- a/tests/application/test_zdo_requests.py +++ b/tests/application/test_zdo_requests.py @@ -1,15 +1,13 @@ import asyncio import pytest -import zigpy.zdo.types as zdo_t import zigpy.types as z_types +import zigpy.zdo.types as zdo_t -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t -from ..conftest import ( - BaseZStackDevice -) +from ..conftest import BaseZStackDevice @pytest.mark.asyncio diff --git a/tests/conftest.py b/tests/conftest.py index c83cd66..3a665ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,21 @@ """Shared fixtures and utilities for testing zigpy-zboss.""" import asyncio -import sys -import inspect -import pytest -import typing import gc +import inspect import logging +import sys +import typing +from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch -from unittest.mock import Mock, PropertyMock, patch, MagicMock, AsyncMock - -from zigpy.zdo import types as zdo_t +import pytest import zigpy +from zigpy.zdo import types as zdo_t +import zigpy_zboss.commands as c import zigpy_zboss.config as conf -from zigpy_zboss.uart import ZbossNcpProtocol import zigpy_zboss.types as t -import zigpy_zboss.commands as c from zigpy_zboss.api import ZBOSS +from zigpy_zboss.uart import ZbossNcpProtocol from zigpy_zboss.zigbee.application import ControllerApplication LOGGER = logging.getLogger(__name__) diff --git a/tests/test_commands.py b/tests/test_commands.py index 29e46e6..fa6f7d0 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,6 +1,5 @@ -import keyword import dataclasses - +import keyword from collections import defaultdict import pytest diff --git a/tests/test_frame.py b/tests/test_frame.py index 2fdd300..1bbc30e 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -1,9 +1,8 @@ import pytest + import zigpy_zboss.types as t -from zigpy_zboss.frames import ( - Frame, InvalidFrame, CRC8, - HLPacket, ZBNCP_LL_BODY_SIZE_MAX, LLHeader -) +from zigpy_zboss.frames import (CRC8, ZBNCP_LL_BODY_SIZE_MAX, Frame, HLPacket, + InvalidFrame, LLHeader) def test_frame_deserialization(): diff --git a/tests/test_nvids.py b/tests/test_nvids.py index 1c09b2b..3d551b5 100644 --- a/tests/test_nvids.py +++ b/tests/test_nvids.py @@ -1,10 +1,10 @@ +from struct import pack + import zigpy_zboss.types as t from zigpy_zboss.types import nvids -from zigpy_zboss.types.nvids import ( - ApsSecureEntry, DSApsSecureKeys, - NwkAddrMapHeader, NwkAddrMapRecord, DSNwkAddrMap -) -from struct import pack +from zigpy_zboss.types.nvids import (ApsSecureEntry, DSApsSecureKeys, + DSNwkAddrMap, NwkAddrMapHeader, + NwkAddrMapRecord) def test_nv_ram_get_byte_size(): diff --git a/tests/test_uart.py b/tests/test_uart.py index e586d72..59c9545 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -1,12 +1,12 @@ import pytest from serial_asyncio import SerialTransport -import zigpy_zboss.config as conf import zigpy_zboss.commands as c +import zigpy_zboss.config as conf import zigpy_zboss.types as t from zigpy_zboss import uart as znp_uart -from zigpy_zboss.frames import Frame from zigpy_zboss.checksum import CRC8 +from zigpy_zboss.frames import Frame @pytest.fixture diff --git a/tests/test_utils.py b/tests/test_utils.py index b54f2e1..e989ae6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,5 @@ -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t from zigpy_zboss.utils import deduplicate_commands diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index f06b6e5..8bf17a5 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -2,22 +2,22 @@ from __future__ import annotations import asyncio -import logging -import itertools import contextlib -import zigpy.state +import itertools +import logging +from collections import Counter, defaultdict + import async_timeout -import zigpy_zboss.types as t -import zigpy_zboss.config as conf +import zigpy.state +import zigpy_zboss.config as conf +import zigpy_zboss.types as t +from zigpy_zboss import commands as c from zigpy_zboss import uart from zigpy_zboss.frames import Frame -from zigpy_zboss import commands as c from zigpy_zboss.nvram import NVRAMHelper -from collections import Counter, defaultdict -from zigpy_zboss.utils import IndicationListener -from zigpy_zboss.utils import BaseResponseListener -from zigpy_zboss.utils import OneShotResponseListener +from zigpy_zboss.utils import (BaseResponseListener, IndicationListener, + OneShotResponseListener) LOGGER = logging.getLogger(__name__) diff --git a/zigpy_zboss/commands/__init__.py b/zigpy_zboss/commands/__init__.py index 3e15e99..7680cbc 100644 --- a/zigpy_zboss/commands/__init__.py +++ b/zigpy_zboss/commands/__init__.py @@ -1,10 +1,10 @@ """Module importing all the commands.""" from .af import AF from .aps import APS -from .zdo import ZDO -from .security import SEC -from .nwk_mgmt import NWK from .ncp_config import NcpConfig +from .nwk_mgmt import NWK +from .security import SEC +from .zdo import ZDO ALL_COMMANDS = [ AF, diff --git a/zigpy_zboss/commands/aps.py b/zigpy_zboss/commands/aps.py index 7dfcf9e..707d59d 100644 --- a/zigpy_zboss/commands/aps.py +++ b/zigpy_zboss/commands/aps.py @@ -1,5 +1,6 @@ """Module defining all APS commands.""" import zigpy.types + import zigpy_zboss.types as t diff --git a/zigpy_zboss/commands/zdo.py b/zigpy_zboss/commands/zdo.py index a96b40e..8c0ebc9 100644 --- a/zigpy_zboss/commands/zdo.py +++ b/zigpy_zboss/commands/zdo.py @@ -1,9 +1,9 @@ """Module defining all ZDO commands.""" from __future__ import annotations -from zigpy.zdo import types as zdo_t import zigpy.types import zigpy.zdo.types +from zigpy.zdo import types as zdo_t import zigpy_zboss.types as t diff --git a/zigpy_zboss/config.py b/zigpy_zboss/config.py index 662a222..4b71ec5 100644 --- a/zigpy_zboss/config.py +++ b/zigpy_zboss/config.py @@ -1,25 +1,15 @@ """Module responsible for configuration.""" -import typing import numbers +import typing import voluptuous as vol -from zigpy.config import ( # noqa: F401 - CONF_NWK, - CONF_DEVICE, - CONF_NWK_KEY, - CONFIG_SCHEMA, - SCHEMA_DEVICE, - CONF_NWK_PAN_ID, - CONF_NWK_CHANNEL, - CONF_DEVICE_PATH, - CONF_NWK_KEY_SEQ, - CONF_NWK_CHANNELS, - CONF_NWK_UPDATE_ID, - CONF_NWK_TC_ADDRESS, - CONF_NWK_TC_LINK_KEY, - CONF_NWK_EXTENDED_PAN_ID, - cv_boolean, -) +from zigpy.config import (CONF_DEVICE, CONF_DEVICE_PATH, # noqa: F401 + CONF_NWK, CONF_NWK_CHANNEL, CONF_NWK_CHANNELS, + CONF_NWK_EXTENDED_PAN_ID, CONF_NWK_KEY, + CONF_NWK_KEY_SEQ, CONF_NWK_PAN_ID, + CONF_NWK_TC_ADDRESS, CONF_NWK_TC_LINK_KEY, + CONF_NWK_UPDATE_ID, CONFIG_SCHEMA, SCHEMA_DEVICE, + cv_boolean) LOG_FILE_NAME = "zigpy-zboss.log" SERIAL_LOG_FILE_NAME = "serial-zigpy-zboss.log" diff --git a/zigpy_zboss/debug.py b/zigpy_zboss/debug.py index 450a3e6..4ef2214 100644 --- a/zigpy_zboss/debug.py +++ b/zigpy_zboss/debug.py @@ -1,10 +1,11 @@ """Module setting up a debugging serial connection with the NCP.""" -import serial import asyncio import logging +import logging.handlers + import async_timeout +import serial import serial_asyncio -import logging.handlers from zigpy_zboss import types as t diff --git a/zigpy_zboss/frames.py b/zigpy_zboss/frames.py index 14229d8..56ad739 100644 --- a/zigpy_zboss/frames.py +++ b/zigpy_zboss/frames.py @@ -4,10 +4,8 @@ import dataclasses import zigpy_zboss.types as t +from zigpy_zboss.checksum import CRC8, CRC16 from zigpy_zboss.exceptions import InvalidFrame -from zigpy_zboss.checksum import CRC8 -from zigpy_zboss.checksum import CRC16 - ZBNCP_LL_BODY_SIZE_MAX = 247 # Check zbncp_ll_pkt.h in ZBOSS NCP host src diff --git a/zigpy_zboss/nvram.py b/zigpy_zboss/nvram.py index 703c729..234fa42 100644 --- a/zigpy_zboss/nvram.py +++ b/zigpy_zboss/nvram.py @@ -1,8 +1,8 @@ """NCP NVRAM related helpers.""" import logging -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t LOGGER = logging.getLogger(__name__) WRITE_DS_LENGTH = 280 diff --git a/zigpy_zboss/tools/factory_reset_ncp.py b/zigpy_zboss/tools/factory_reset_ncp.py index be98a2f..9f4b6bd 100644 --- a/zigpy_zboss/tools/factory_reset_ncp.py +++ b/zigpy_zboss/tools/factory_reset_ncp.py @@ -1,11 +1,11 @@ """Script to factory reset the coordinator.""" +import asyncio import sys + import serial -import asyncio -from zigpy_zboss.api import ZBOSS from zigpy_zboss import types as t - +from zigpy_zboss.api import ZBOSS from zigpy_zboss.tools.config import get_config diff --git a/zigpy_zboss/tools/get_ncp_version.py b/zigpy_zboss/tools/get_ncp_version.py index 594890a..bb9ec0a 100644 --- a/zigpy_zboss/tools/get_ncp_version.py +++ b/zigpy_zboss/tools/get_ncp_version.py @@ -1,9 +1,9 @@ """Script to print the NCP firmware version.""" -import serial import asyncio -from zigpy_zboss.api import ZBOSS +import serial +from zigpy_zboss.api import ZBOSS from zigpy_zboss.tools.config import get_config diff --git a/zigpy_zboss/types/__init__.py b/zigpy_zboss/types/__init__.py index 210722b..a78217f 100644 --- a/zigpy_zboss/types/__init__.py +++ b/zigpy_zboss/types/__init__.py @@ -1,7 +1,7 @@ """Module importing all types.""" from .basic import * # noqa: F401, F403 +from .commands import * # noqa: F401, F403 +from .cstruct import * # noqa: F401, F403 from .named import * # noqa: F401, F403 from .nvids import * # noqa: F401, F403 -from .cstruct import * # noqa: F401, F403 from .structs import * # noqa: F401, F403 -from .commands import * # noqa: F401, F403 diff --git a/zigpy_zboss/types/basic.py b/zigpy_zboss/types/basic.py index 2eca2d7..d8f270a 100644 --- a/zigpy_zboss/types/basic.py +++ b/zigpy_zboss/types/basic.py @@ -1,8 +1,9 @@ """Module defining basic types.""" from __future__ import annotations + import typing -from zigpy.types import int8s, uint8_t, enum_factory # noqa: F401 +from zigpy.types import enum_factory, int8s, uint8_t # noqa: F401 from zigpy_zboss.types.cstruct import CStruct @@ -31,18 +32,9 @@ class bitmap16(enum.IntFlag): """Bitmap with 16 bits value.""" else: - from zigpy.types import ( # noqa: F401 - enum8, - enum16, - bitmap8, - bitmap16, - uint16_t, - uint24_t, - uint32_t, - uint40_t, - uint56_t, - uint64_t, - ) + from zigpy.types import (bitmap8, bitmap16, enum8, enum16, # noqa: F401 + uint16_t, uint24_t, uint32_t, uint40_t, uint56_t, + uint64_t) class enum24(enum_factory(uint24_t)): """Enum with 24 bits value.""" diff --git a/zigpy_zboss/types/commands.py b/zigpy_zboss/types/commands.py index c359f58..4300156 100644 --- a/zigpy_zboss/types/commands.py +++ b/zigpy_zboss/types/commands.py @@ -1,8 +1,9 @@ """Module defining types used for commands.""" from __future__ import annotations + +import dataclasses import enum import logging -import dataclasses import zigpy.zdo.types @@ -455,9 +456,7 @@ def to_frame(self, *, align=False): if self._partial: raise ValueError(f"Cannot serialize a partial frame: {self}") - from zigpy_zboss.frames import HLPacket - from zigpy_zboss.frames import LLHeader - from zigpy_zboss.frames import Frame + from zigpy_zboss.frames import Frame, HLPacket, LLHeader chunks = [] diff --git a/zigpy_zboss/types/cstruct.py b/zigpy_zboss/types/cstruct.py index fb306e7..fa560b0 100644 --- a/zigpy_zboss/types/cstruct.py +++ b/zigpy_zboss/types/cstruct.py @@ -1,9 +1,9 @@ """Module defining cstruct types.""" from __future__ import annotations -import typing -import inspect import dataclasses +import inspect +import typing import zigpy.types as zigpy_t diff --git a/zigpy_zboss/types/named.py b/zigpy_zboss/types/named.py index 4c43ccc..d99201c 100644 --- a/zigpy_zboss/types/named.py +++ b/zigpy_zboss/types/named.py @@ -1,24 +1,13 @@ """Module defining named types.""" from __future__ import annotations -import typing -import logging import dataclasses +import logging +import typing -from zigpy.types import ( # noqa: F401 - NWK, - List, - Bool, - PanId, - EUI64, - Struct, - bitmap8, - KeyData, - Channels, - ClusterId, - ExtendedPanId, - CharacterString, -) +from zigpy.types import (EUI64, NWK, Bool, Channels, # noqa: F401 + CharacterString, ClusterId, ExtendedPanId, KeyData, + List, PanId, Struct, bitmap8) from . import basic diff --git a/zigpy_zboss/types/nvids.py b/zigpy_zboss/types/nvids.py index 64023cd..9abd972 100644 --- a/zigpy_zboss/types/nvids.py +++ b/zigpy_zboss/types/nvids.py @@ -1,7 +1,10 @@ """Module defining zboss nvram types.""" from __future__ import annotations + import zigpy.types as t + import zigpy_zboss.types as zboss_t + from . import basic diff --git a/zigpy_zboss/types/structs.py b/zigpy_zboss/types/structs.py index b86ec4f..803c82e 100644 --- a/zigpy_zboss/types/structs.py +++ b/zigpy_zboss/types/structs.py @@ -1,5 +1,6 @@ """Module defining struct types.""" import zigpy.types as t + from . import basic diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index 5700753..ec45a2b 100644 --- a/zigpy_zboss/uart.py +++ b/zigpy_zboss/uart.py @@ -1,16 +1,18 @@ """Module that connects and sends/receives bytes from the nRF52 SoC.""" -import typing import asyncio import logging -import zigpy.serial +import typing + import async_timeout import serial # type: ignore +import zigpy.serial + import zigpy_zboss.config as conf from zigpy_zboss import types as t -from zigpy_zboss.frames import Frame from zigpy_zboss.checksum import CRC8 -from zigpy_zboss.logger import SERIAL_LOGGER from zigpy_zboss.exceptions import InvalidFrame +from zigpy_zboss.frames import Frame +from zigpy_zboss.logger import SERIAL_LOGGER LOGGER = logging.getLogger(__name__) ACK_TIMEOUT = 1 diff --git a/zigpy_zboss/utils.py b/zigpy_zboss/utils.py index 4346572..71f1961 100644 --- a/zigpy_zboss/utils.py +++ b/zigpy_zboss/utils.py @@ -1,10 +1,11 @@ """Module defining utility functions.""" from __future__ import annotations -import typing import asyncio -import logging import dataclasses +import logging +import typing + import zigpy_zboss.types as t LOGGER = logging.getLogger(__name__) diff --git a/zigpy_zboss/zigbee/application.py b/zigpy_zboss/zigbee/application.py index 7fee917..1bdc17d 100644 --- a/zigpy_zboss/zigbee/application.py +++ b/zigpy_zboss/zigbee/application.py @@ -1,27 +1,29 @@ """ControllerApplication for ZBOSS NCP protocol based adapters.""" import asyncio import logging -import zigpy.util -import zigpy.state +from typing import Any, Dict + +import async_timeout import zigpy.appdb +import zigpy.application import zigpy.config import zigpy.device -import async_timeout import zigpy.endpoint import zigpy.exceptions +import zigpy.state import zigpy.types as t -import zigpy.application -import zigpy_zboss.types as t_zboss +import zigpy.util import zigpy.zdo.types as zdo_t -import zigpy_zboss.config as conf +from zigpy.exceptions import DeliveryError -from typing import Any, Dict -from zigpy_zboss.api import ZBOSS +import zigpy_zboss.config as conf +import zigpy_zboss.types as t_zboss from zigpy_zboss import commands as c -from zigpy.exceptions import DeliveryError -from .device import ZbossCoordinator, ZbossDevice -from zigpy_zboss.exceptions import ZbossResponseError +from zigpy_zboss.api import ZBOSS from zigpy_zboss.config import CONFIG_SCHEMA, SCHEMA_DEVICE +from zigpy_zboss.exceptions import ZbossResponseError + +from .device import ZbossCoordinator, ZbossDevice LOGGER = logging.getLogger(__name__) diff --git a/zigpy_zboss/zigbee/device.py b/zigpy_zboss/zigbee/device.py index 875b69c..6525f21 100644 --- a/zigpy_zboss/zigbee/device.py +++ b/zigpy_zboss/zigbee/device.py @@ -1,15 +1,16 @@ """Zigbee device object.""" import logging -import zigpy.util +from typing import Any + import zigpy.device import zigpy.endpoint import zigpy.types as t -import zigpy_zboss.types as t_zboss -from typing import Any -from zigpy_zboss import commands as c -from zigpy.zdo import types as zdo_t +import zigpy.util from zigpy.zdo import ZDO as ZigpyZDO +from zigpy.zdo import types as zdo_t +import zigpy_zboss.types as t_zboss +from zigpy_zboss import commands as c LOGGER = logging.getLogger(__name__) From e692f69eb5180bb95294844843ede5bef2bfa392 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:19:38 +0400 Subject: [PATCH 36/57] cleanup --- tests/__init__.py | 1 + tests/api/test_listeners.py | 5 ++ tests/api/test_request.py | 10 +++- tests/api/test_response.py | 11 ++++ tests/application/test_connect.py | 22 ++++++-- tests/application/test_join.py | 15 +++-- tests/application/test_requests.py | 61 +++++++++++--------- tests/application/test_startup.py | 32 ++++++----- tests/application/test_zdo_requests.py | 21 ++++--- tests/application/test_zigpy_callbacks.py | 2 +- tests/conftest.py | 68 ++++++++++++++++++++--- tests/test_commands.py | 7 +++ tests/test_config.py | 3 + tests/test_frame.py | 1 + tests/test_nvids.py | 1 + tests/test_types_basic.py | 11 ++++ tests/test_types_cstruct.py | 12 ++++ tests/test_types_named.py | 8 ++- tests/test_uart.py | 13 +++++ tests/test_utils.py | 3 + 20 files changed, 236 insertions(+), 71 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..b4c17f5 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for zigpy-zboss.""" diff --git a/tests/api/test_listeners.py b/tests/api/test_listeners.py index 7edf73c..319793d 100644 --- a/tests/api/test_listeners.py +++ b/tests/api/test_listeners.py @@ -1,3 +1,4 @@ +"""Test listeners.""" import asyncio from unittest.mock import call @@ -10,6 +11,7 @@ @pytest.mark.asyncio async def test_resolve(event_loop, mocker): + """Test listener resolution.""" callback = mocker.Mock() callback_listener = IndicationListener( [c.NcpConfig.GetZigbeeRole.Rsp( @@ -67,6 +69,7 @@ async def test_resolve(event_loop, mocker): @pytest.mark.asyncio async def test_cancel(event_loop): + """Test cancelling one-shot listener.""" # Cancelling a one-shot listener prevents it from being fired future = event_loop.create_future() one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( @@ -91,6 +94,7 @@ async def test_cancel(event_loop): @pytest.mark.asyncio async def test_multi_cancel(event_loop, mocker): + """Test cancelling indication listener.""" callback = mocker.Mock() callback_listener = IndicationListener( [c.NcpConfig.GetZigbeeRole.Rsp( @@ -136,6 +140,7 @@ async def test_multi_cancel(event_loop, mocker): @pytest.mark.asyncio async def test_api_cancel_listeners(connected_zboss, mocker): + """Test cancel listeners from api.""" zboss, zboss_server = connected_zboss callback = mocker.Mock() diff --git a/tests/api/test_request.py b/tests/api/test_request.py index a908b2b..27e63ed 100644 --- a/tests/api/test_request.py +++ b/tests/api/test_request.py @@ -1,3 +1,4 @@ +"""Test api requests.""" import asyncio import logging @@ -12,6 +13,7 @@ @pytest.mark.asyncio async def test_cleanup_timeout_internal(connected_zboss): + """Test internal cleanup timeout.""" zboss, zboss_server = connected_zboss assert not any(zboss._listeners.values()) @@ -25,6 +27,7 @@ async def test_cleanup_timeout_internal(connected_zboss): @pytest.mark.asyncio async def test_cleanup_timeout_external(connected_zboss): + """Test external cleanup timeout.""" zboss, zboss_server = connected_zboss assert not any(zboss._listeners.values()) @@ -40,6 +43,7 @@ async def test_cleanup_timeout_external(connected_zboss): @pytest.mark.asyncio async def test_zboss_request_kwargs(connected_zboss, event_loop): + """Test zboss request.""" zboss, zboss_server = connected_zboss # Invalid format @@ -86,7 +90,8 @@ async def send_ping_response(): @pytest.mark.asyncio -async def test_zboss_sreq_srsp(connected_zboss, event_loop): +async def test_zboss_req_rsp(connected_zboss, event_loop): + """Test zboss request/response.""" zboss, zboss_server = connected_zboss # Each SREQ must have a corresponding SRSP, so this will fail @@ -114,6 +119,7 @@ async def send_ping_response(): @pytest.mark.asyncio async def test_zboss_unknown_frame(connected_zboss, caplog): + """Test zboss unknown frame.""" zboss, _ = connected_zboss hl_header = t.HLCommonHeader( version=0x0121, type=0xFFFF, id=0x123421 @@ -131,6 +137,7 @@ async def test_zboss_unknown_frame(connected_zboss, caplog): @pytest.mark.asyncio async def test_send_failure_when_disconnected(connected_zboss): + """Test send failure when disconnected.""" zboss, _ = connected_zboss zboss._uart = None @@ -143,6 +150,7 @@ async def test_send_failure_when_disconnected(connected_zboss): @pytest.mark.asyncio async def test_frame_merge(connected_zboss, mocker): + """Test frame fragmentation.""" zboss, zboss_server = connected_zboss large_data = b"a" * (ZBNCP_LL_BODY_SIZE_MAX * 2 + 50) diff --git a/tests/api/test_response.py b/tests/api/test_response.py index a2a33b5..0dd4bb7 100644 --- a/tests/api/test_response.py +++ b/tests/api/test_response.py @@ -1,3 +1,4 @@ +"""Test response.""" import asyncio import async_timeout @@ -10,6 +11,7 @@ @pytest.mark.asyncio async def test_responses(connected_zboss): + """Test responses.""" zboss, zboss_server = connected_zboss assert not any(zboss._listeners.values()) @@ -41,6 +43,7 @@ async def test_responses(connected_zboss): @pytest.mark.asyncio async def test_responses_multiple(connected_zboss): + """Test multiple responses.""" zboss, _ = connected_zboss assert not any(zboss._listeners.values()) @@ -85,6 +88,7 @@ async def test_responses_multiple(connected_zboss): @pytest.mark.asyncio async def test_response_timeouts(connected_zboss): + """Test future response timeouts.""" zboss, _ = connected_zboss response = c.NcpConfig.GetZigbeeRole.Rsp( @@ -134,6 +138,7 @@ async def send_soon(delay): @pytest.mark.asyncio async def test_response_matching_partial(connected_zboss): + """Test partial response matching.""" zboss, _ = connected_zboss future = zboss.wait_for_response( @@ -174,6 +179,7 @@ async def test_response_matching_partial(connected_zboss): @pytest.mark.asyncio async def test_response_matching_exact(connected_zboss): + """Test exact response matching.""" zboss, _ = connected_zboss response1 = c.NcpConfig.GetZigbeeRole.Rsp( @@ -208,6 +214,7 @@ async def test_response_matching_exact(connected_zboss): @pytest.mark.asyncio async def test_response_not_matching_out_of_order(connected_zboss): + """Test not matching response.""" zboss, _ = connected_zboss response = c.NcpConfig.GetZigbeeRole.Rsp( @@ -227,6 +234,7 @@ async def test_response_not_matching_out_of_order(connected_zboss): @pytest.mark.asyncio async def test_wait_responses_empty(connected_zboss): + """Test wait empty response.""" zboss, _ = connected_zboss # You shouldn't be able to wait for an empty list of responses @@ -236,6 +244,7 @@ async def test_wait_responses_empty(connected_zboss): @pytest.mark.asyncio async def test_response_callback_simple(connected_zboss, event_loop, mocker): + """Test simple response callback.""" zboss, _ = connected_zboss sync_callback = mocker.Mock() @@ -264,6 +273,7 @@ async def test_response_callback_simple(connected_zboss, event_loop, mocker): @pytest.mark.asyncio async def test_response_callbacks(connected_zboss, event_loop, mocker): + """Test response callbacks.""" zboss, _ = connected_zboss sync_callback = mocker.Mock() @@ -398,6 +408,7 @@ async def async_callback(response): @pytest.mark.asyncio async def test_wait_for_responses(connected_zboss, event_loop): + """Test wait for responses.""" zboss, _ = connected_zboss response1 = c.NcpConfig.GetZigbeeRole.Rsp( diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index 59e6864..133d9c3 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -1,3 +1,4 @@ +"""Test application connect.""" import asyncio from unittest.mock import AsyncMock, patch @@ -9,11 +10,12 @@ from zigpy_zboss.uart import connect as uart_connect from zigpy_zboss.zigbee.application import ControllerApplication -from ..conftest import BaseServerZBOSS, BaseZStackDevice +from ..conftest import BaseServerZBOSS, BaseZbossDevice @pytest.mark.asyncio async def test_no_double_connect(make_zboss_server, mocker): + """Test no multiple connection.""" zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) app = mocker.Mock() @@ -32,6 +34,7 @@ async def test_no_double_connect(make_zboss_server, mocker): @pytest.mark.asyncio async def test_leak_detection(make_zboss_server, mocker): + """Test leak detection.""" zboss_server = make_zboss_server(server_cls=BaseServerZBOSS) def count_connected(): @@ -60,6 +63,7 @@ def count_connected(): @pytest.mark.asyncio async def test_probe_unsuccessful_slow(make_zboss_server, mocker): + """Test unsuccessful probe.""" zboss_server = make_zboss_server( server_cls=BaseServerZBOSS, shorten_delays=False ) @@ -82,6 +86,7 @@ async def test_probe_unsuccessful_slow(make_zboss_server, mocker): @pytest.mark.asyncio async def test_probe_successful(make_zboss_server, event_loop): + """Test successful probe.""" zboss_server = make_zboss_server( server_cls=BaseServerZBOSS, shorten_delays=False ) @@ -107,8 +112,9 @@ async def send_ping_response(): @pytest.mark.asyncio async def test_probe_multiple(make_application): + """Test multiple probe.""" # Make sure that our listeners don't get cleaned up after each probe - app, zboss_server = make_application(server_cls=BaseZStackDevice) + app, zboss_server = make_application(server_cls=BaseZbossDevice) app.close = lambda: None @@ -126,7 +132,8 @@ async def test_probe_multiple(make_application): @pytest.mark.asyncio async def test_shutdown_from_app(mocker, make_application, event_loop): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test shutdown from application.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) @@ -143,7 +150,8 @@ async def test_shutdown_from_app(mocker, make_application, event_loop): @pytest.mark.asyncio async def test_clean_shutdown(make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test clean shutdown.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) # This should not throw @@ -154,7 +162,8 @@ async def test_clean_shutdown(make_application): @pytest.mark.asyncio async def test_multiple_shutdown(make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test multiple shutdown.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) await app.shutdown() @@ -168,7 +177,8 @@ async def test_multiple_shutdown(make_application): new=0.1 ) async def test_watchdog(make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test the watchdog.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) app._watchdog_feed = AsyncMock(wraps=app._watchdog_feed) await app.startup(auto_form=False) diff --git a/tests/application/test_join.py b/tests/application/test_join.py index e94ff2f..269d11f 100644 --- a/tests/application/test_join.py +++ b/tests/application/test_join.py @@ -1,3 +1,4 @@ +"""Test application device joining.""" import asyncio import pytest @@ -8,12 +9,13 @@ import zigpy_zboss.commands as c import zigpy_zboss.types as t -from ..conftest import BaseZStackDevice +from ..conftest import BaseZbossDevice @pytest.mark.asyncio async def test_permit_join(mocker, make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test permit join.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) permit_join_coordinator = zboss_server.reply_once_to( request=c.ZDO.PermitJoin.Req( @@ -43,7 +45,8 @@ async def test_permit_join(mocker, make_application): @pytest.mark.asyncio async def test_join_coordinator(make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test coordinator join.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) # Handle us opening joins on the coordinator permit_join_coordinator = zboss_server.reply_once_to( @@ -73,10 +76,11 @@ async def test_join_coordinator(make_application): @pytest.mark.asyncio async def test_join_device(make_application): + """Test device join.""" ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") nwk = 0x1234 - app, zboss_server = make_application(server_cls=BaseZStackDevice) + app, zboss_server = make_application(server_cls=BaseZbossDevice) app.add_initialized_device(ieee=ieee, nwk=nwk) permit_join = zboss_server.reply_once_to( @@ -105,7 +109,8 @@ async def test_join_device(make_application): @pytest.mark.asyncio async def test_on_zdo_device_join(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test ZDO device join indication listener.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.patch.object(app, "handle_join", wraps=app.handle_join) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 909b6e7..c66a55f 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -1,20 +1,23 @@ +"""Test application requests.""" import asyncio from unittest.mock import AsyncMock as CoroutineMock import pytest import zigpy.endpoint import zigpy.profiles +import zigpy.types as zigpy_t import zigpy_zboss.commands as c import zigpy_zboss.config as conf import zigpy_zboss.types as t -from ..conftest import BaseZStackDevice +from ..conftest import BaseZbossDevice @pytest.mark.asyncio async def test_zigpy_request(make_application): - app, zboss_server = make_application(BaseZStackDevice) + """Test zigpy request.""" + app, zboss_server = make_application(BaseZbossDevice) await app.startup(auto_form=False) device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) @@ -130,14 +133,15 @@ async def test_zigpy_request(make_application): @pytest.mark.parametrize( "addr", [ - zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.IEEE, - address=t.EUI64(range(8))), - zigpy.types.AddrModeAddress(addr_mode=zigpy.types.AddrMode.NWK, - address=t.NWK(0xAABB)), + zigpy.types.AddrModeAddress( + addr_mode=zigpy.types.AddrMode.IEEE, address=t.EUI64(range(8))), + zigpy.types.AddrModeAddress( + addr_mode=zigpy.types.AddrMode.NWK, address=t.NWK(0xAABB)), ], ) async def test_request_addr_mode(addr, make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test address mode request.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) @@ -164,7 +168,8 @@ async def test_request_addr_mode(addr, make_application, mocker): @pytest.mark.asyncio async def test_mrequest(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test multicast request.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) mocker.patch.object(app, "send_packet", new=CoroutineMock()) group = app.groups.add_group(0x1234, "test group") @@ -173,20 +178,20 @@ async def test_mrequest(make_application, mocker): assert app.send_packet.call_count == 1 assert ( - app.send_packet.mock_calls[0].args[0].dst - == zigpy.types.AddrModeAddress( - addr_mode=zigpy.types.AddrMode.Group, address=0x1234 - ) + app.send_packet.mock_calls[0].args[0].dst == + zigpy.types.AddrModeAddress( + addr_mode=zigpy.types.AddrMode.Group, address=0x1234) ) - assert app.send_packet.mock_calls[0].args[ - 0].data.serialize() == b"\x01\x01\x01" + assert app.send_packet.mock_calls[0].args[0].data.serialize() == \ + b"\x01\x01\x01" await app.shutdown() @pytest.mark.asyncio async def test_mrequest_doesnt_block(make_application, event_loop): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test non blocking multicast request.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) zboss_server.reply_once_to( request=c.APS.DataReq.Req( @@ -239,7 +244,8 @@ async def on_request_sent(): @pytest.mark.asyncio async def test_broadcast(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test broadcast request.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup() zboss_server.reply_once_to( request=c.APS.DataReq.Req(TSN=1, ParamLength=21, DataLength=3, @@ -283,8 +289,9 @@ async def test_broadcast(make_application, mocker): @pytest.mark.asyncio async def test_request_concurrency(make_application, mocker): + """Test request concurency.""" app, zboss_server = make_application( - server_cls=BaseZStackDevice, + server_cls=BaseZbossDevice, client_config={conf.CONF_MAX_CONCURRENT_REQUESTS: 2}, ) @@ -360,9 +367,9 @@ async def callback(req): @pytest.mark.asyncio async def test_request_cancellation_shielding( - make_application, mocker, event_loop -): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + make_application, mocker, event_loop): + """Test request cancellation shielding.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) @@ -460,7 +467,8 @@ async def inner(): @pytest.mark.asyncio async def test_send_security_and_packet_source_route(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test sending security and packet source route.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) packet = zigpy_t.ZigbeePacket( @@ -522,17 +530,18 @@ async def test_send_security_and_packet_source_route(make_application, mocker): @pytest.mark.asyncio async def test_send_packet_failure_disconnected(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test sending packet failure at disconnect.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) app._api = None packet = zigpy_t.ZigbeePacket( - src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, - address=0x0000), + src=zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), src_ep=0x9A, - dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, - address=0xEEFF), + dst=zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, address=0xEEFF), dst_ep=0xBC, tsn=0xDE, profile_id=0x1234, diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index 21e3efa..0a6aea0 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -1,23 +1,21 @@ -import pytest +"""Test application startup.""" from unittest.mock import AsyncMock as CoroutineMock import pytest -import voluptuous as vol from zigpy.exceptions import NetworkNotFormed import zigpy_zboss.commands as c import zigpy_zboss.types as t from zigpy_zboss.api import ZBOSS -from ..conftest import BaseZStackDevice, BaseZStackGenericDevice +from ..conftest import BaseZbossDevice, BaseZbossGenericDevice + @pytest.mark.asyncio -async def test_info( - make_application, - caplog, -): +async def test_info(make_application, caplog): + """Test network information.""" app, zboss_server = make_application( - server_cls=BaseZStackGenericDevice, active_sequence=True + server_cls=BaseZbossGenericDevice, active_sequence=True ) pan_id = 0x5679 @@ -200,7 +198,8 @@ async def test_info( @pytest.mark.asyncio async def test_endpoints(make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test endpoints.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) endpoints = [] zboss_server.register_indication_listener( @@ -218,8 +217,9 @@ async def test_endpoints(make_application): @pytest.mark.asyncio async def test_not_configured(make_application): + """Test device not configured.""" app, zboss_server = make_application( - server_cls=BaseZStackGenericDevice, active_sequence=True + server_cls=BaseZbossGenericDevice, active_sequence=True ) # Simulate responses for each request in load_network_info @@ -261,7 +261,8 @@ async def test_not_configured(make_application): @pytest.mark.asyncio async def test_reset(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test application reset.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) # `_reset` should be called at least once # to put the radio into a consistent state @@ -276,7 +277,8 @@ async def test_reset(make_application, mocker): @pytest.mark.asyncio async def test_auto_form_unnecessary(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test unnecessary auto form.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) mocker.patch.object(app, "form_network", new=CoroutineMock()) await app.startup(auto_form=True) @@ -288,7 +290,8 @@ async def test_auto_form_unnecessary(make_application, mocker): @pytest.mark.asyncio async def test_auto_form_necessary(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test necessary auto form.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) assert app.state.network_info.channel == 0 assert app.state.network_info.channel_mask == t.Channels.NO_CHANNELS @@ -303,7 +306,8 @@ async def test_auto_form_necessary(make_application, mocker): @pytest.mark.asyncio async def test_concurrency_auto_config(make_application): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test auto config concurrency.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.connect() await app.start_network() diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py index 32691ee..15f5b62 100644 --- a/tests/application/test_zdo_requests.py +++ b/tests/application/test_zdo_requests.py @@ -1,3 +1,4 @@ +"""Test application ZDO request.""" import asyncio import pytest @@ -7,18 +8,17 @@ import zigpy_zboss.commands as c import zigpy_zboss.types as t -from ..conftest import BaseZStackDevice +from ..conftest import BaseZbossDevice @pytest.mark.asyncio -async def test_mgmt_nwk_update_req( - make_application, mocker -): +async def test_mgmt_nwk_update_req(make_application, mocker): + """Test ZDO_MGMT_NWK_UPDATE_REQ request.""" mocker.patch( "zigpy.application.CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S", 0.1 ) - app, zboss_server = make_application(server_cls=BaseZStackDevice) + app, zboss_server = make_application(server_cls=BaseZbossDevice) new_channel = 11 old_channel = 1 @@ -32,9 +32,14 @@ async def update_channel(req): zboss_server.reply_once_to( request=c.APS.DataReq.Req( - TSN=123, ParamLength=21, DataLength=3, - ProfileID=260, ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_req, - DstEndpoint=0, partial=True), + TSN=123, + ParamLength=21, + DataLength=3, + ProfileID=260, + ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_req, + DstEndpoint=0, + partial=True + ), responses=[c.APS.DataReq.Rsp( TSN=123, StatusCat=t.StatusCategory(1), diff --git a/tests/application/test_zigpy_callbacks.py b/tests/application/test_zigpy_callbacks.py index 0733b39..b342933 100644 --- a/tests/application/test_zigpy_callbacks.py +++ b/tests/application/test_zigpy_callbacks.py @@ -4,8 +4,8 @@ import zigpy.types as zigpy_t import zigpy.zdo.types as zdo_t -import zigpy_zboss.types as t import zigpy_zboss.commands as c +import zigpy_zboss.types as t from ..conftest import BaseZStackDevice, serialize_zdo_command diff --git a/tests/conftest.py b/tests/conftest.py index 3a665ac..9562f30 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,7 @@ # Globally handle async tests and error on unawaited coroutines def pytest_collection_modifyitems(session, config, items): + """Modify collection items.""" for item in items: item.add_marker( pytest.mark.filterwarnings( @@ -36,7 +37,7 @@ def pytest_collection_modifyitems(session, config, items): @pytest.hookimpl(trylast=True) def pytest_fixture_post_finalizer(fixturedef, request) -> None: - """Called after fixture teardown""" + """Post fixture teardown.""" if fixturedef.argname != "event_loop": return @@ -75,11 +76,10 @@ def event_loop( class ForwardingSerialTransport: - """ - Serial transport that hooks directly into a protocol - """ + """Serial transport that hooks directly into a protocol.""" def __init__(self, protocol): + """Initailize.""" self.protocol = protocol self._is_connected = False self.other = None @@ -100,12 +100,14 @@ def _connect(self): self.other.protocol.connection_made(self) def write(self, data): + """Write.""" assert self._is_connected self.protocol.data_received(data) def close( self, *, error=ValueError("Connection was closed") # noqa: B008 ): + """Close.""" LOGGER.debug("Closing %s", self) if not self._is_connected: @@ -120,10 +122,12 @@ def close( self.protocol.connection_lost(error) def __repr__(self): + """Representation.""" return f"<{type(self).__name__} to {self.protocol}>" def config_for_port_path(path): + """Port path configuration.""" return conf.CONFIG_SCHEMA( { conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: path}, @@ -135,6 +139,7 @@ def config_for_port_path(path): @pytest.fixture def make_zboss_server(mocker): + """Instantiate a zboss server.""" transports = [] def inner(server_cls, config=None, shorten_delays=True): @@ -208,6 +213,7 @@ def passthrough_serial_conn( @pytest.fixture def make_connected_zboss(make_zboss_server, mocker): + """Make a connection fixture.""" async def inner(server_cls): config = conf.CONFIG_SCHEMA( { @@ -230,6 +236,7 @@ async def inner(server_cls): @pytest.fixture def connected_zboss(event_loop, make_connected_zboss): + """Zboss connected fixture.""" zboss, zboss_server = event_loop.run_until_complete( make_connected_zboss(BaseServerZBOSS) ) @@ -238,6 +245,7 @@ def connected_zboss(event_loop, make_connected_zboss): def reply_to(request): + """Reply to decorator.""" def inner(function): if not hasattr(function, "_reply_to"): function._reply_to = [] @@ -250,12 +258,14 @@ def inner(function): def serialize_zdo_command(command_id, **kwargs): + """ZDO command serialization.""" field_names, field_types = zdo_t.CLUSTERS[command_id] return t.Bytes(zigpy.types.serialize(kwargs.values(), field_types)) def deserialize_zdo_command(command_id, data): + """ZDO command deserialization.""" field_names, field_types = zdo_t.CLUSTERS[command_id] args, data = zigpy.types.deserialize(data, field_types) @@ -263,6 +273,8 @@ def deserialize_zdo_command(command_id, data): class BaseServerZBOSS(ZBOSS): + """Base ZBOSS server.""" + align_structs = False version = None @@ -296,6 +308,7 @@ async def _send_responses(self, request, responses): await self.send(response) def reply_once_to(self, request, responses, *, override=False): + """Reply once to.""" if override: self._listeners[request.header].clear() @@ -310,6 +323,7 @@ async def replier(): return asyncio.create_task(replier()) def reply_to(self, request, responses, *, override=False): + """Reply to.""" if override: self._listeners[request.header].clear() @@ -326,16 +340,19 @@ async def callback(request): return callback async def send(self, response): + """Send.""" if response is not None and self._uart is not None: await self._uart.send(response.to_frame(align=self.align_structs)) def close(self): + """Close.""" # We don't clear listeners on shutdown with patch.object(self, "_listeners", {}): return super().close() def simple_deepcopy(d): + """Get a deepcopy.""" if not hasattr(d, "copy"): return d @@ -350,6 +367,7 @@ def simple_deepcopy(d): def merge_dicts(a, b): + """Merge dicts.""" c = simple_deepcopy(a) for key, value in b.items(): @@ -363,6 +381,7 @@ def merge_dicts(a, b): @pytest.fixture def make_application(make_zboss_server): + """Application fixture.""" def inner( server_cls, client_config=None, @@ -447,8 +466,8 @@ async def energy_scan(self, channels, duration_exp, count): def zdo_request_matcher( - dst_addr, command_id: t.uint16_t, **kwargs -): + dst_addr, command_id: t.uint16_t, **kwargs): + """Request matcher.""" zdo_kwargs = {k: v for k, v in kwargs.items() if k.startswith("zdo_")} kwargs = {k: v for k, v in kwargs.items() if not k.startswith("zdo_")} @@ -468,8 +487,11 @@ def zdo_request_matcher( ) -class BaseZStackDevice(BaseServerZBOSS): +class BaseZbossDevice(BaseServerZBOSS): + """Base ZBOSS Device.""" + def __init__(self, *args, **kwargs): + """Initialize.""" super().__init__(*args, **kwargs) self.active_endpoints = [] self._nvram = {} @@ -483,11 +505,13 @@ def __init__(self, *args, **kwargs): self.reply_to(request=req, responses=[func]) def connection_lost(self, exc): + """Lost connection.""" self.active_endpoints.clear() return super().connection_lost(exc) @reply_to(c.NcpConfig.GetJoinStatus.Req(partial=True)) def get_join_status(self, request): + """Handle get join status.""" return c.NcpConfig.GetJoinStatus.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -497,6 +521,7 @@ def get_join_status(self, request): @reply_to(c.NcpConfig.NCPModuleReset.Req(partial=True)) def get_ncp_reset(self, request): + """Handle NCP reset.""" return c.NcpConfig.NCPModuleReset.Rsp( TSN=0xFF, StatusCat=t.StatusCategory(1), @@ -505,6 +530,7 @@ def get_ncp_reset(self, request): @reply_to(c.NcpConfig.GetShortAddr.Req(partial=True)) def get_short_addr(self, request): + """Handle get short address.""" return c.NcpConfig.GetShortAddr.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -514,6 +540,7 @@ def get_short_addr(self, request): @reply_to(c.APS.DataReq.Req(partial=True, DstEndpoint=0)) def on_zdo_request(self, req): + """Handle APS Data request.""" return c.APS.DataReq.Rsp( TSN=req.TSN, StatusCat=t.StatusCategory(1), @@ -527,6 +554,7 @@ def on_zdo_request(self, req): @reply_to(c.NcpConfig.GetLocalIEEE.Req(partial=True)) def get_local_ieee(self, request): + """Handle get local IEEE.""" return c.NcpConfig.GetLocalIEEE.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -537,6 +565,7 @@ def get_local_ieee(self, request): @reply_to(c.NcpConfig.GetZigbeeRole.Req(partial=True)) def get_zigbee_role(self, request): + """Handle get zigbee role.""" return c.NcpConfig.GetZigbeeRole.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -546,6 +575,7 @@ def get_zigbee_role(self, request): @reply_to(c.NcpConfig.GetExtendedPANID.Req(partial=True)) def get_extended_panid(self, request): + """Handle get extended PANID.""" return c.NcpConfig.GetExtendedPANID.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -555,6 +585,7 @@ def get_extended_panid(self, request): @reply_to(c.ZDO.PermitJoin.Req(partial=True)) def get_permit_join(self, request): + """Handle get permit join.""" return c.ZDO.PermitJoin.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -563,6 +594,7 @@ def get_permit_join(self, request): @reply_to(c.NcpConfig.GetShortPANID.Req(partial=True)) def get_short_panid(self, request): + """Handle get short PANID.""" return c.NcpConfig.GetShortPANID.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -572,6 +604,7 @@ def get_short_panid(self, request): @reply_to(c.NcpConfig.GetCurrentChannel.Req(partial=True)) def get_current_channel(self, request): + """Handle get current channel.""" if self.new_channel != 0: channel = self.new_channel else: @@ -587,6 +620,7 @@ def get_current_channel(self, request): @reply_to(c.NcpConfig.GetChannelMask.Req(partial=True)) def get_channel_mask(self, request): + """Handle get channel mask.""" return c.NcpConfig.GetChannelMask.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -597,6 +631,7 @@ def get_channel_mask(self, request): @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) def read_nvram(self, request): + """Handle NVRAM read.""" status_code = 1 if request.DatasetId == t.DatasetId.ZB_NVRAM_COMMON_DATA: status_code = 0 @@ -702,6 +737,7 @@ def read_nvram(self, request): @reply_to(c.NcpConfig.GetTrustCenterAddr.Req(partial=True)) def get_trust_center_addr(self, request): + """Handle get trust center address.""" return c.NcpConfig.GetTrustCenterAddr.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -712,6 +748,7 @@ def get_trust_center_addr(self, request): @reply_to(c.NcpConfig.GetRxOnWhenIdle.Req(partial=True)) def get_rx_on_when_idle(self, request): + """Handle get RX on when idle.""" return c.NcpConfig.GetRxOnWhenIdle.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -721,6 +758,7 @@ def get_rx_on_when_idle(self, request): @reply_to(c.NWK.StartWithoutFormation.Req(partial=True)) def start_without_formation(self, request): + """Handle start without formation.""" return c.NWK.StartWithoutFormation.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -729,6 +767,7 @@ def start_without_formation(self, request): @reply_to(c.NcpConfig.GetModuleVersion.Req(partial=True)) def get_module_version(self, request): + """Handle get module version.""" return c.NcpConfig.GetModuleVersion.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -740,6 +779,7 @@ def get_module_version(self, request): @reply_to(c.AF.SetSimpleDesc.Req(partial=True)) def set_simple_desc(self, request): + """Handle set simple descriptor.""" return c.AF.SetSimpleDesc.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -748,6 +788,7 @@ def set_simple_desc(self, request): @reply_to(c.NcpConfig.GetEDTimeout.Req(partial=True)) def get_ed_timeout(self, request): + """Handle get EndDevice timeout.""" return c.NcpConfig.GetEDTimeout.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -757,6 +798,7 @@ def get_ed_timeout(self, request): @reply_to(c.NcpConfig.GetMaxChildren.Req(partial=True)) def get_max_children(self, request): + """Handle get max children.""" return c.NcpConfig.GetMaxChildren.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -766,6 +808,7 @@ def get_max_children(self, request): @reply_to(c.NcpConfig.GetAuthenticationStatus.Req(partial=True)) def get_authentication_status(self, request): + """Handle get authentication status.""" return c.NcpConfig.GetAuthenticationStatus.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -775,6 +818,7 @@ def get_authentication_status(self, request): @reply_to(c.NcpConfig.GetParentAddr.Req(partial=True)) def get_parent_addr(self, request): + """Handle get parent address.""" return c.NcpConfig.GetParentAddr.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -784,6 +828,7 @@ def get_parent_addr(self, request): @reply_to(c.NcpConfig.GetCoordinatorVersion.Req(partial=True)) def get_coordinator_version(self, request): + """Handle get coordinator version.""" return c.NcpConfig.GetCoordinatorVersion.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), @@ -792,6 +837,7 @@ def get_coordinator_version(self, request): ) def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): + """Handle node description request.""" if NWKAddrOfInterest != 0x0000: return @@ -837,8 +883,11 @@ def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): return responses -class BaseZStackGenericDevice(BaseServerZBOSS): +class BaseZbossGenericDevice(BaseServerZBOSS): + """Base ZBOSS generic device.""" + def __init__(self, *args, **kwargs): + """Init method.""" super().__init__(*args, **kwargs) self.active_endpoints = [] self._nvram = {} @@ -851,11 +900,13 @@ def __init__(self, *args, **kwargs): self.reply_to(request=req, responses=[func]) def connection_lost(self, exc): + """Lost connection.""" self.active_endpoints.clear() return super().connection_lost(exc) @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) def read_nvram(self, request): + """Handle NVRAM read.""" status_code = 1 if request.DatasetId == t.DatasetId.ZB_NVRAM_COMMON_DATA: status_code = 0 @@ -960,6 +1011,7 @@ def read_nvram(self, request): ) def on_zdo_node_desc_req(self, req, NWKAddrOfInterest): + """Handle node description request.""" if NWKAddrOfInterest != 0x0000: return diff --git a/tests/test_commands.py b/tests/test_commands.py index fa6f7d0..3a70a2b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,3 +1,4 @@ +"""Test commands.""" import dataclasses import keyword from collections import defaultdict @@ -9,6 +10,7 @@ def _validate_schema(schema): + """Validate the schema for command parameters.""" for index, param in enumerate(schema): assert isinstance(param.name, str) assert param.name.isidentifier() @@ -22,6 +24,7 @@ def _validate_schema(schema): def test_commands_schema(): + """Test the schema of all commands.""" commands_by_id = defaultdict(list) for commands in c.ALL_COMMANDS: @@ -69,6 +72,7 @@ def test_commands_schema(): def test_command_param_binding(): + """Test if commands match correctly.""" # Example for GetModuleVersion which only requires TSN c.NcpConfig.GetModuleVersion.Req(TSN=1) @@ -137,6 +141,7 @@ def test_command_param_binding(): def test_command_optional_params(): + """Test optional parameters.""" # Basic response with required parameters only basic_ieee_addr_rsp = c.ZDO.IeeeAddrReq.Rsp( TSN=10, @@ -180,6 +185,7 @@ def test_command_optional_params(): def test_command_optional_params_failures(): + """Test optional parameters failures.""" with pytest.raises(KeyError): # Optional params cannot be skipped over c.ZDO.IeeeAddrReq.Rsp( @@ -212,6 +218,7 @@ def test_command_optional_params_failures(): def test_simple_descriptor(): + """Test simple descriptor.""" lvlist16_type = t.LVList[t.uint16_t] simple_descriptor = t.SimpleDescriptor() diff --git a/tests/test_config.py b/tests/test_config.py index 3f4c084..e96a502 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,4 @@ +"""Test config.""" import pytest from voluptuous import Invalid @@ -5,6 +6,7 @@ def test_pin_states_same_lengths(): + """Test same lengths pin states.""" # Bare schema works conf.CONFIG_SCHEMA( { @@ -44,6 +46,7 @@ def test_pin_states_same_lengths(): def test_pin_states_different_lengths(): + """Test different lengths pin states.""" # They must be the same length with pytest.raises(Invalid): conf.CONFIG_SCHEMA( diff --git a/tests/test_frame.py b/tests/test_frame.py index 1bbc30e..d4397ae 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -1,3 +1,4 @@ +"""Test frame.""" import pytest import zigpy_zboss.types as t diff --git a/tests/test_nvids.py b/tests/test_nvids.py index 3d551b5..015b576 100644 --- a/tests/test_nvids.py +++ b/tests/test_nvids.py @@ -1,3 +1,4 @@ +"""Test NVIDS.""" from struct import pack import zigpy_zboss.types as t diff --git a/tests/test_types_basic.py b/tests/test_types_basic.py index bec532f..4a8c8e8 100644 --- a/tests/test_types_basic.py +++ b/tests/test_types_basic.py @@ -1,9 +1,11 @@ +"""Test basic types.""" import pytest import zigpy_zboss.types as t def test_enum(): + """Test enum.""" class TestEnum(t.bitmap16): ALL = 0xFFFF CH_1 = 0x0001 @@ -26,6 +28,7 @@ class TestEnum(t.bitmap16): def test_int_too_short(): + """Test int too short.""" with pytest.raises(ValueError): t.uint8_t.deserialize(b"") @@ -34,6 +37,7 @@ def test_int_too_short(): def test_int_out_of_bounds(): + """Test int out of bounds.""" with pytest.raises(ValueError): t.uint8_t(-1) @@ -42,6 +46,7 @@ def test_int_out_of_bounds(): def test_bytes(): + """Test bytes.""" data = b"abcde\x00\xff" r, rest = t.Bytes.deserialize(data) @@ -76,6 +81,7 @@ def test_bytes(): def test_longbytes(): + """Test long bytes.""" data = b"abcde\x00\xff" * 50 extra = b"\xffrest of the data\x00" @@ -102,6 +108,7 @@ def test_longbytes(): def test_lvlist(): + """Test lvlist.""" class TestList(t.LVList, item_type=t.uint8_t, length_type=t.uint8_t): pass @@ -117,6 +124,7 @@ class TestList(t.LVList, item_type=t.uint8_t, length_type=t.uint8_t): def test_lvlist_too_short(): + """Test lvlist too short.""" class TestList(t.LVList, item_type=t.uint8_t, length_type=t.uint8_t): pass @@ -128,6 +136,7 @@ class TestList(t.LVList, item_type=t.uint8_t, length_type=t.uint8_t): def test_fixed_list(): + """Test fixed list.""" class TestList(t.FixedList, item_type=t.uint16_t, length=3): pass @@ -145,6 +154,7 @@ class TestList(t.FixedList, item_type=t.uint16_t, length=3): def test_fixed_list_deserialize(): + """Test fixed list deserialize.""" class TestList(t.FixedList, length=3, item_type=t.uint16_t): pass @@ -159,6 +169,7 @@ class TestList(t.FixedList, length=3, item_type=t.uint16_t): def test_enum_instance_types(): + """Test enum instance.""" class TestEnum(t.enum8): Member = 0x00 diff --git a/tests/test_types_cstruct.py b/tests/test_types_cstruct.py index a1ddecb..90bbaae 100644 --- a/tests/test_types_cstruct.py +++ b/tests/test_types_cstruct.py @@ -1,9 +1,11 @@ +"""Test cstruct types.""" import pytest import zigpy_zboss.types as t def test_struct_fields(): + """Test struct fields.""" class TestStruct(t.CStruct): a: t.uint8_t b: t.uint16_t @@ -18,6 +20,7 @@ class TestStruct(t.CStruct): def test_struct_field_values(): + """Test struct field values.""" class TestStruct(t.CStruct): a: t.uint8_t b: t.uint16_t @@ -42,6 +45,7 @@ class TestStruct(t.CStruct): def test_struct_methods_and_constants(): + """Test struct methods and constants.""" class TestStruct(t.CStruct): a: t.uint8_t b: t.uint16_t @@ -68,6 +72,7 @@ def annotated_method(self: "TestStruct") -> int: def test_struct_nesting(): + """Test struct nesting.""" class Outer(t.CStruct): e: t.uint32_t @@ -98,6 +103,7 @@ class Inner(t.CStruct): def test_struct_aligned_serialization_deserialization(): + """Test struct aligned serialization/deserialization.""" class TestStruct(t.CStruct): a: t.uint8_t # One padding byte here @@ -138,6 +144,7 @@ class TestStruct(t.CStruct): def test_struct_aligned_nested_serialization_deserialization(): + """Test structed alined nested serialization/deserialization.""" class Inner(t.CStruct): _padding_byte = b"\xCD" @@ -173,6 +180,7 @@ class TestStruct(t.CStruct): def test_struct_unaligned_serialization_deserialization(): + """Test struct unaligned serialization/deserialization.""" class TestStruct(t.CStruct): a: t.uint8_t b: t.uint16_t @@ -204,6 +212,7 @@ class TestStruct(t.CStruct): def test_struct_equality(): + """Test struct equality.""" class InnerStruct(t.CStruct): c: t.EUI64 @@ -243,6 +252,7 @@ class TestStruct2(t.CStruct): def test_struct_repr(): + """Test struct representation.""" class TestStruct(t.CStruct): a: t.uint8_t b: t.uint32_t @@ -252,6 +262,7 @@ class TestStruct(t.CStruct): def test_struct_bad_fields(): + """Test struct bad fields.""" with pytest.raises(TypeError): class TestStruct(t.CStruct): @@ -260,6 +271,7 @@ class TestStruct(t.CStruct): def test_struct_incomplete_serialization(): + """Test struct incomplete serialization.""" class TestStruct(t.CStruct): a: t.uint8_t b: t.uint8_t diff --git a/tests/test_types_named.py b/tests/test_types_named.py index 31cc0da..1e34527 100644 --- a/tests/test_types_named.py +++ b/tests/test_types_named.py @@ -1,11 +1,15 @@ +"""Test named types.""" import pytest import zigpy_zboss.types as t def test_channel_entry(): - """Test ChannelEntry class for proper serialization, - deserialization, equality, and representation.""" + """Test channel entry. + + ChannelEntry class for proper serialization, + deserialization, equality, and representation. + """ # Sample data for testing page_data = b"\x01" # Page number as bytes channel_mask_data = b"\x00\x01\x00\x00" # Sample channel mask as bytes diff --git a/tests/test_uart.py b/tests/test_uart.py index 59c9545..cc2dfd9 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -1,3 +1,4 @@ +"""Test uart.""" import pytest from serial_asyncio import SerialTransport @@ -11,6 +12,7 @@ @pytest.fixture def connected_uart(mocker): + """Uart connected fixture.""" zboss = mocker.Mock() config = { conf.CONF_DEVICE_PATH: "/dev/ttyACM0", @@ -32,6 +34,7 @@ def ll_checksum(frame): @pytest.fixture def dummy_serial_conn(event_loop, mocker): + """Connect serial dummy.""" device = "/dev/ttyACM0" serial_interface = mocker.Mock() @@ -65,6 +68,7 @@ def create_serial_conn(loop, protocol_factory, url, *args, **kwargs): def test_uart_rx_basic(connected_uart): + """Test UART basic receive.""" znp, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( @@ -85,6 +89,7 @@ def test_uart_rx_basic(connected_uart): def test_uart_str_repr(connected_uart): + """Test uart representation.""" znp, uart = connected_uart str(uart) @@ -92,6 +97,7 @@ def test_uart_str_repr(connected_uart): def test_uart_rx_byte_by_byte(connected_uart): + """Test uart RX byte by byte.""" znp, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( @@ -113,6 +119,7 @@ def test_uart_rx_byte_by_byte(connected_uart): def test_uart_rx_byte_by_byte_garbage(connected_uart): + """Test uart RX byte by byte garbage.""" znp, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( @@ -144,6 +151,7 @@ def test_uart_rx_byte_by_byte_garbage(connected_uart): def test_uart_rx_big_garbage(connected_uart): + """Test uart RX big garbage.""" znp, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( @@ -174,6 +182,7 @@ def test_uart_rx_big_garbage(connected_uart): def test_uart_rx_corrupted_fcs(connected_uart): + """Test uart RX corrupted.""" znp, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( @@ -196,6 +205,7 @@ def test_uart_rx_corrupted_fcs(connected_uart): def test_uart_rx_sof_stress(connected_uart): + """Test uart RX signature stress.""" znp, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( @@ -224,6 +234,7 @@ def test_uart_rx_sof_stress(connected_uart): def test_uart_frame_received_error(connected_uart, mocker): + """Test uart frame received error.""" znp, uart = connected_uart znp.frame_received = mocker.Mock(side_effect=RuntimeError("An error")) @@ -249,6 +260,7 @@ def test_uart_frame_received_error(connected_uart, mocker): @pytest.mark.asyncio async def test_connection_lost(dummy_serial_conn, mocker, event_loop): + """Test connection lost.""" device, _ = dummy_serial_conn znp = mocker.Mock() @@ -268,6 +280,7 @@ async def test_connection_lost(dummy_serial_conn, mocker, event_loop): @pytest.mark.asyncio async def test_connection_made(dummy_serial_conn, mocker): + """Test connection made.""" device, _ = dummy_serial_conn znp = mocker.Mock() diff --git a/tests/test_utils.py b/tests/test_utils.py index e989ae6..ff4b775 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,11 @@ +"""Test utils.""" import zigpy_zboss.commands as c import zigpy_zboss.types as t from zigpy_zboss.utils import deduplicate_commands def test_command_deduplication_simple(): + """Test command deduplication simple.""" c1 = c.NcpConfig.GetModuleVersion.Req(TSN=10) c2 = c.NcpConfig.NCPModuleReset.Req(TSN=10, Option=t.ResetOptions(0)) @@ -15,6 +17,7 @@ def test_command_deduplication_simple(): def test_command_deduplication_complex(): + """Test command deduplication complex.""" result = deduplicate_commands( [ c.NcpConfig.GetModuleVersion.Rsp( From f70601a1826b500f87944970dc6a627dbd3013e1 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:18:05 +0400 Subject: [PATCH 37/57] fix circular import --- zigpy_zboss/types/commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/zigpy_zboss/types/commands.py b/zigpy_zboss/types/commands.py index 4300156..72de59b 100644 --- a/zigpy_zboss/types/commands.py +++ b/zigpy_zboss/types/commands.py @@ -7,7 +7,8 @@ import zigpy.zdo.types -import zigpy_zboss.types as t +import zigpy_zboss.types.basic as t +import zigpy_zboss.types.named as t_named LOGGER = logging.getLogger(__name__) TYPE_ZBOSS_NCP_API_HL = t.uint8_t(0x06) @@ -719,7 +720,7 @@ class Relationship(t.enum8): STATUS_SCHEMA = ( - t.Param("TSN", t.uint8_t, "Transmit Sequence Number"), - t.Param("StatusCat", StatusCategory, "Status category code"), - t.Param("StatusCode", t.uint8_t, "Status code inside category"), + t_named.Param("TSN", t.uint8_t, "Transmit Sequence Number"), + t_named.Param("StatusCat", StatusCategory, "Status category code"), + t_named.Param("StatusCode", t.uint8_t, "Status code inside category"), ) From 8e0c6a28e80267fa9d55bf539b775e46e9a4ea95 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:48:17 +0400 Subject: [PATCH 38/57] update pytest CI job --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2a4e39..60609c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,7 @@ jobs: Run tests Python ${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 id: python @@ -164,7 +164,7 @@ jobs: allow-prereleases: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3 + uses: actions/cache@v2 with: fail-on-cache-miss: true path: venv From cb51057ce8f4cf66b91799f7189cb67bc4cb05c9 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:58:20 +0400 Subject: [PATCH 39/57] update pytest job --- .github/workflows/ci.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60609c1..99adbae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,21 +157,24 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v2 id: python with: python-version: ${{ matrix.python-version }} - allow-prereleases: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v2 with: - fail-on-cache-miss: true path: venv key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ - hashFiles('setup.py', 'requirements_test.txt', 'requirements.txt', 'pyproject.toml', 'setup.cfg') }} + hashFiles('pyproject.toml') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 - name: Register Python problem matcher run: | echo "::add-matcher::.github/workflows/matchers/python.json" From e54988e76a863ed6ba3bc81eab0528df9f49d6a5 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:09:59 +0400 Subject: [PATCH 40/57] update zigpy callbacks test --- tests/application/test_zigpy_callbacks.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/application/test_zigpy_callbacks.py b/tests/application/test_zigpy_callbacks.py index b342933..2d8fe5e 100644 --- a/tests/application/test_zigpy_callbacks.py +++ b/tests/application/test_zigpy_callbacks.py @@ -1,3 +1,4 @@ +"""Test zigpy callbacks.""" import asyncio import pytest @@ -7,12 +8,13 @@ import zigpy_zboss.commands as c import zigpy_zboss.types as t -from ..conftest import BaseZStackDevice, serialize_zdo_command +from ..conftest import BaseZbossDevice, serialize_zdo_command @pytest.mark.asyncio async def test_on_zdo_device_announce_nwk_change(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test device announce network address change.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.spy(app, "handle_join") @@ -67,7 +69,8 @@ async def test_on_zdo_device_announce_nwk_change(make_application, mocker): @pytest.mark.asyncio async def test_on_zdo_device_leave_callback(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test ZDO device leave indication.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.patch.object(app, "handle_leave") @@ -87,7 +90,8 @@ async def test_on_zdo_device_leave_callback(make_application, mocker): @pytest.mark.asyncio async def test_on_af_message_callback(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test AF message indication.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.patch.object(app, "packet_received") @@ -199,7 +203,8 @@ async def test_on_af_message_callback(make_application, mocker): @pytest.mark.asyncio async def test_receive_zdo_broadcast(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test receive ZDO broadcast.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.patch.object(app, "packet_received") @@ -237,7 +242,8 @@ async def test_receive_zdo_broadcast(make_application, mocker): @pytest.mark.asyncio async def test_receive_af_broadcast(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test receive AF broadcast.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.patch.object(app, "packet_received") @@ -279,7 +285,8 @@ async def test_receive_af_broadcast(make_application, mocker): @pytest.mark.asyncio async def test_receive_af_group(make_application, mocker): - app, zboss_server = make_application(server_cls=BaseZStackDevice) + """Test receive AF group.""" + app, zboss_server = make_application(server_cls=BaseZbossDevice) await app.startup(auto_form=False) mocker.patch.object(app, "packet_received") From 03c56e03052c3701b2124196287411791c3ef2cf Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:16:27 +0400 Subject: [PATCH 41/57] update CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99adbae..7413a15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: python -m venv venv . venv/bin/activate pip install -U pip setuptools pre-commit + pip install -r requirements_test.txt pip install -e . pre-commit: From 560dc5a658794a74d341e91294d55459de90e760 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:29:49 +0400 Subject: [PATCH 42/57] increment cache version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7413a15..cbce72c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: env: CODE_FOLDER: zigpy_zboss - CACHE_VERSION: 2 + CACHE_VERSION: 3 DEFAULT_PYTHON: 3.10.8 PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit From 44d90e69d044bd8a1e4631b4af219051f165ccef Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:33:39 +0400 Subject: [PATCH 43/57] do not test against python<3.10 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbce72c..5652ae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12"] + python-version: ["3.10.8", "3.11.0", "3.12"] steps: - name: Check out code from GitHub uses: actions/checkout@v2 @@ -151,7 +151,7 @@ jobs: needs: prepare-base strategy: matrix: - python-version: ["3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12"] + python-version: ["3.10.8", "3.11.0", "3.12"] name: >- Run tests Python ${{ matrix.python-version }} steps: From a097f12fa198e8dfc89f84ea43edb2cfc7888f04 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:48:04 +0400 Subject: [PATCH 44/57] update --- tests/test_types_named.py | 2 +- zigpy_zboss/types/commands.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_types_named.py b/tests/test_types_named.py index 1e34527..94440fb 100644 --- a/tests/test_types_named.py +++ b/tests/test_types_named.py @@ -31,7 +31,7 @@ def test_channel_entry(): assert channel_entry != t.ChannelEntry(page=0, channel_mask=0x0200) # Test __repr__ - expected_repr = "ChannelEntry(page=1, channels=)" + expected_repr = "ChannelEntry(page=1, channels=)" assert repr(channel_entry) == expected_repr # Test handling of None types for page or channel_mask diff --git a/zigpy_zboss/types/commands.py b/zigpy_zboss/types/commands.py index 72de59b..3453195 100644 --- a/zigpy_zboss/types/commands.py +++ b/zigpy_zboss/types/commands.py @@ -423,7 +423,8 @@ def __init__(self, *, partial=False, **params): issubclass(param.type, (t.ShortBytes, t.LongBytes)), isinstance(value, list) and issubclass(param.type, list), - isinstance(value, bool) and issubclass(param.type, t.Bool), + isinstance( + value, bool) and issubclass(param.type, t_named.Bool), ] # fmt: on From 1dd076c31514ae59a6b6ce92727b855f109691a0 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:15:18 +0400 Subject: [PATCH 45/57] fix named tests --- tests/test_types_named.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_types_named.py b/tests/test_types_named.py index 94440fb..887386a 100644 --- a/tests/test_types_named.py +++ b/tests/test_types_named.py @@ -12,7 +12,7 @@ def test_channel_entry(): """ # Sample data for testing page_data = b"\x01" # Page number as bytes - channel_mask_data = b"\x00\x01\x00\x00" # Sample channel mask as bytes + channel_mask_data = b"\x00\x10\x00\x00" # Sample channel mask as bytes data = page_data + channel_mask_data @@ -20,18 +20,19 @@ def test_channel_entry(): channel_entry, remaining_data = t.ChannelEntry.deserialize(data) assert remaining_data == b'' # no extra data should remain assert channel_entry.page == 1 - assert channel_entry.channel_mask == 0x0100 + assert channel_entry.channel_mask == 0x00001000 # Test serialization assert channel_entry.serialize() == data # Test equality - another_entry = t.ChannelEntry(page=1, channel_mask=0x0100) + another_entry = t.ChannelEntry(page=1, channel_mask=0x00001000) assert channel_entry == another_entry - assert channel_entry != t.ChannelEntry(page=0, channel_mask=0x0200) + assert channel_entry != t.ChannelEntry(page=0, channel_mask=0x00002000) # Test __repr__ - expected_repr = "ChannelEntry(page=1, channels=)" + expected_repr = \ + "ChannelEntry(page=1, channels=)" assert repr(channel_entry) == expected_repr # Test handling of None types for page or channel_mask From 2c72e6200db4fafc3d5e828cceefa210fc372573 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:00:28 +0400 Subject: [PATCH 46/57] Run actions on every push and PR --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5652ae9..8fb3a22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,7 @@ name: CI # yamllint disable-line rule:truthy on: push: - branches: - - add-unit-tests - # pull_request: ~ + pull_request: ~ env: CODE_FOLDER: zigpy_zboss From 58075a0ea184e0f6c7cf3e01d26f12e04b224522 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:58:44 +0400 Subject: [PATCH 47/57] fix serial mock --- tests/conftest.py | 2 +- tests/test_uart.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9562f30..e71f658 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -199,7 +199,7 @@ def passthrough_serial_conn( return fut mocker.patch( - "serial_asyncio.create_serial_connection", + "zigpy.serial.pyserial_asyncio.create_serial_connection", new=passthrough_serial_conn ) diff --git a/tests/test_uart.py b/tests/test_uart.py index cc2dfd9..0bb394a 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -61,7 +61,8 @@ def create_serial_conn(loop, protocol_factory, url, *args, **kwargs): return fut mocker.patch( - "serial_asyncio.create_serial_connection", new=create_serial_conn + "zigpy.serial.pyserial_asyncio.create_serial_connection", + new=create_serial_conn ) return device, serial_interface From 5b46264daa361099c59697f67ac80c3b7ed828ba Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 11:58:49 +0400 Subject: [PATCH 48/57] update StatusCode values type + flake8 --- tests/api/test_listeners.py | 26 ++-- tests/api/test_request.py | 10 +- tests/api/test_response.py | 92 ++++++------- tests/application/test_connect.py | 2 +- tests/application/test_join.py | 6 +- tests/application/test_requests.py | 12 +- tests/application/test_startup.py | 90 +++++++++---- tests/application/test_zdo_requests.py | 4 +- tests/conftest.py | 52 ++++---- tests/test_commands.py | 174 ++++++++++++++----------- tests/test_uart.py | 14 +- tests/test_utils.py | 18 +-- zigpy_zboss/types/commands.py | 7 +- 13 files changed, 286 insertions(+), 221 deletions(-) diff --git a/tests/api/test_listeners.py b/tests/api/test_listeners.py index 319793d..f36e3d8 100644 --- a/tests/api/test_listeners.py +++ b/tests/api/test_listeners.py @@ -17,7 +17,7 @@ async def test_resolve(event_loop, mocker): [c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) )], callback ) @@ -26,20 +26,20 @@ async def test_resolve(event_loop, mocker): one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) )], future) match = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) no_match = c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3, @@ -75,7 +75,7 @@ async def test_cancel(event_loop): one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )], future) one_shot_listener.cancel() @@ -83,7 +83,7 @@ async def test_cancel(event_loop): match = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) assert not one_shot_listener.resolve(match) @@ -100,7 +100,7 @@ async def test_multi_cancel(event_loop, mocker): [c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )], callback ) @@ -109,20 +109,20 @@ async def test_multi_cancel(event_loop, mocker): one_shot_listener = OneShotResponseListener([c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )], future) match = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) no_match = c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3, @@ -149,7 +149,7 @@ async def test_api_cancel_listeners(connected_zboss, mocker): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), callback ) @@ -158,13 +158,13 @@ async def test_api_cancel_listeners(connected_zboss, mocker): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3, diff --git a/tests/api/test_request.py b/tests/api/test_request.py index 27e63ed..53a6344 100644 --- a/tests/api/test_request.py +++ b/tests/api/test_request.py @@ -58,7 +58,7 @@ async def test_zboss_request_kwargs(connected_zboss, event_loop): ping_rsp = c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 @@ -78,7 +78,7 @@ async def send_ping_response(): await zboss.request(c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) )) @@ -103,7 +103,7 @@ async def test_zboss_req_rsp(connected_zboss, event_loop): ping_rsp = c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 @@ -157,7 +157,7 @@ async def test_frame_merge(connected_zboss, mocker): command = c.NcpConfig.ReadNVRAM.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, NVRAMVersion=t.uint16_t(0x0000), DatasetId=t.DatasetId(0x0000), Dataset=t.NVRAMDataset(large_data), @@ -171,7 +171,7 @@ async def test_frame_merge(connected_zboss, mocker): c.NcpConfig.ReadNVRAM.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, NVRAMVersion=t.uint16_t(0x0000), DatasetId=t.DatasetId(0x0000), Dataset=t.NVRAMDataset(large_data), diff --git a/tests/api/test_response.py b/tests/api/test_response.py index 0dd4bb7..8f76ac2 100644 --- a/tests/api/test_response.py +++ b/tests/api/test_response.py @@ -20,7 +20,7 @@ async def test_responses(connected_zboss): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )) @@ -29,7 +29,7 @@ async def test_responses(connected_zboss): response = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) await zboss_server.send(response) @@ -51,26 +51,26 @@ async def test_responses_multiple(connected_zboss): future1 = zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )) future2 = zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )) future3 = zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )) response = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) zboss.frame_received(response.to_frame()) @@ -94,7 +94,7 @@ async def test_response_timeouts(connected_zboss): response = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) @@ -108,7 +108,7 @@ async def send_soon(delay): assert (await zboss.wait_for_response(c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ))) == response @@ -126,7 +126,7 @@ async def send_soon(delay): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )) ) == response @@ -145,7 +145,7 @@ async def test_response_matching_partial(connected_zboss): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(2), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ) ) @@ -153,19 +153,19 @@ async def test_response_matching_partial(connected_zboss): response1 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) response2 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(2), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) response3 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) @@ -185,19 +185,19 @@ async def test_response_matching_exact(connected_zboss): response1 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) response2 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(2) ) response3 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=11, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) @@ -220,7 +220,7 @@ async def test_response_not_matching_out_of_order(connected_zboss): response = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) zboss.frame_received(response.to_frame()) @@ -252,7 +252,7 @@ async def test_response_callback_simple(connected_zboss, event_loop, mocker): good_response = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) bad_response = c.NcpConfig.GetZigbeeRole.Rsp( @@ -292,26 +292,26 @@ async def async_callback(response): good_response1 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) good_response2 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(2) ) good_response3 = c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 ) bad_response1 = c.ZDO.MgtLeave.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20) + StatusCode=t.StatusCodeGeneric.OK) bad_response2 = c.NcpConfig.GetModuleVersion.Req(TSN=1) responses = [ @@ -319,20 +319,20 @@ async def async_callback(response): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ), c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ), # Matching against different response types should also work c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 @@ -340,19 +340,19 @@ async def async_callback(response): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=4 @@ -363,13 +363,13 @@ async def async_callback(response): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ), c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 @@ -377,7 +377,7 @@ async def async_callback(response): c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=4 @@ -414,26 +414,26 @@ async def test_wait_for_responses(connected_zboss, event_loop): response1 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) response2 = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(2) ) response3 = c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 ) response4 = c.ZDO.MgtLeave.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20) + StatusCode=t.StatusCodeGeneric.OK) response5 = c.NcpConfig.GetModuleVersion.Req(TSN=1) # We shouldn't see any effects from receiving a frame early @@ -444,12 +444,12 @@ async def test_wait_for_responses(connected_zboss, event_loop): [c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ), c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True )] ) @@ -460,7 +460,7 @@ async def test_wait_for_responses(connected_zboss, event_loop): c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 @@ -468,7 +468,7 @@ async def test_wait_for_responses(connected_zboss, event_loop): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(10) ), ] @@ -478,7 +478,7 @@ async def test_wait_for_responses(connected_zboss, event_loop): future3 = zboss.wait_for_responses([c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=4 @@ -491,7 +491,7 @@ async def test_wait_for_responses(connected_zboss, event_loop): c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3 @@ -499,19 +499,19 @@ async def test_wait_for_responses(connected_zboss, event_loop): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=4 @@ -552,7 +552,7 @@ async def test_wait_for_responses(connected_zboss, event_loop): zboss.frame_received(c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=4 @@ -561,7 +561,7 @@ async def test_wait_for_responses(connected_zboss, event_loop): assert (await future3) == c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=4 diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index 133d9c3..e4bdffe 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -95,7 +95,7 @@ async def test_probe_successful(make_zboss_server, event_loop): ping_rsp = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1), ) diff --git a/tests/application/test_join.py b/tests/application/test_join.py index 269d11f..26d0020 100644 --- a/tests/application/test_join.py +++ b/tests/application/test_join.py @@ -28,7 +28,7 @@ async def test_permit_join(mocker, make_application): c.ZDO.PermitJoin.Rsp( TSN=123, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ), ], ) @@ -61,7 +61,7 @@ async def test_join_coordinator(make_application): c.ZDO.PermitJoin.Rsp( TSN=123, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ), ], ) @@ -94,7 +94,7 @@ async def test_join_device(make_application): c.ZDO.PermitJoin.Rsp( TSN=123, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ) ], ) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index c66a55f..5cd58a8 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -48,7 +48,7 @@ async def test_zigpy_request(make_application): responses=[c.APS.DataReq.Rsp( TSN=1, StatusCat=t.StatusCategory(4), - StatusCode=1, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), DstEndpoint=1, SrcEndpoint=1, @@ -206,7 +206,7 @@ async def test_mrequest_doesnt_block(make_application, event_loop): c.APS.DataReq.Rsp( TSN=1, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), DstEndpoint=1, SrcEndpoint=1, @@ -262,7 +262,7 @@ async def test_broadcast(make_application, mocker): c.APS.DataReq.Rsp( TSN=1, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=t.EUI64.convert("00:00:00:00:00:00:ff:fd"), DstEndpoint=255, SrcEndpoint=1, @@ -323,7 +323,7 @@ async def callback(req): await zboss_server.send(c.APS.DataReq.Rsp( TSN=req.TSN, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=req.DstAddr, DstEndpoint=req.DstEndpoint, SrcEndpoint=req.SrcEndpoint, @@ -433,7 +433,7 @@ async def inner(): c.APS.DataReq.Rsp( TSN=1, StatusCat=t.StatusCategory(4), - StatusCode=1, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), DstEndpoint=1, SrcEndpoint=1, @@ -508,7 +508,7 @@ async def test_send_security_and_packet_source_route(make_application, mocker): c.APS.DataReq.Rsp( TSN=1, StatusCat=t.StatusCategory(4), - StatusCode=1, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), DstEndpoint=1, SrcEndpoint=1, diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index 0a6aea0..a22ce20 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -28,35 +28,45 @@ async def test_info(make_application, caplog): zboss_server.reply_once_to( request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), responses=[c.NcpConfig.GetZigbeeRole.Rsp( - TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), responses=[c.NcpConfig.GetZigbeeRole.Rsp( - TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetJoinStatus.Req(TSN=2), responses=[c.NcpConfig.GetJoinStatus.Rsp( - TSN=2, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=2, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, Joined=1)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetShortAddr.Req(TSN=3), responses=[c.NcpConfig.GetShortAddr.Rsp( - TSN=3, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=3, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, NWKAddr=t.NWK(0xAABB))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetLocalIEEE.Req(TSN=4, MacInterfaceNum=0), responses=[c.NcpConfig.GetLocalIEEE.Rsp( - TSN=4, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=4, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, MacInterfaceNum=0, IEEE=t.EUI64(range(8)))] ) @@ -64,35 +74,45 @@ async def test_info(make_application, caplog): zboss_server.reply_once_to( request=c.NcpConfig.GetZigbeeRole.Req(TSN=5), responses=[c.NcpConfig.GetZigbeeRole.Rsp( - TSN=5, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=5, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(2))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetExtendedPANID.Req(TSN=6), responses=[c.NcpConfig.GetExtendedPANID.Rsp( - TSN=6, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=6, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, ExtendedPANID=ext_pan_id)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetShortPANID.Req(TSN=7), responses=[c.NcpConfig.GetShortPANID.Rsp( - TSN=7, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=7, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, PANID=t.PanId(pan_id))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetCurrentChannel.Req(TSN=8), responses=[c.NcpConfig.GetCurrentChannel.Rsp( - TSN=8, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=8, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, Channel=channel, Page=0)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetChannelMask.Req(TSN=9), responses=[c.NcpConfig.GetChannelMask.Rsp( - TSN=9, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=9, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, ChannelList=[t.ChannelEntry(page=1, channel_mask=channel_mask)])] ) @@ -103,7 +123,7 @@ async def test_info(make_application, caplog): responses=[c.NcpConfig.GetTrustCenterAddr.Rsp( TSN=12, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, TCIEEE=t.EUI64.convert("00:11:22:33:44:55:66:77") # Example Trust Center IEEE address )] @@ -112,42 +132,54 @@ async def test_info(make_application, caplog): zboss_server.reply_once_to( request=c.NcpConfig.GetRxOnWhenIdle.Req(TSN=13), responses=[c.NcpConfig.GetRxOnWhenIdle.Rsp( - TSN=13, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=13, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, RxOnWhenIdle=1)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetEDTimeout.Req(TSN=14), responses=[c.NcpConfig.GetEDTimeout.Rsp( - TSN=14, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=14, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, Timeout=t.TimeoutIndex(0x00))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetMaxChildren.Req(TSN=15), responses=[c.NcpConfig.GetMaxChildren.Rsp( - TSN=15, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=15, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, ChildrenNbr=10)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetAuthenticationStatus.Req(TSN=16), responses=[c.NcpConfig.GetAuthenticationStatus.Rsp( - TSN=16, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=16, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, Authenticated=True)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetParentAddr.Req(TSN=17), responses=[c.NcpConfig.GetParentAddr.Rsp( - TSN=17, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=17, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, NWKParentAddr=parent_address)] ) zboss_server.reply_once_to( request=c.NcpConfig.GetCoordinatorVersion.Req(TSN=18), responses=[c.NcpConfig.GetCoordinatorVersion.Rsp( - TSN=18, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=18, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, CoordinatorVersion=coordinator_version)] ) @@ -159,7 +191,9 @@ async def test_info(make_application, caplog): TCSignificance=t.uint8_t(0x01), ), responses=[c.ZDO.PermitJoin.Rsp( - TSN=20, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=20, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, )] ) @@ -168,7 +202,9 @@ async def test_info(make_application, caplog): TSN=21, Option=t.ResetOptions(0) ), responses=[c.NcpConfig.NCPModuleReset.Rsp( - TSN=21, StatusCat=t.StatusCategory(4), StatusCode=1 + TSN=21, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK )] ) @@ -226,21 +262,27 @@ async def test_not_configured(make_application): zboss_server.reply_once_to( request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), responses=[c.NcpConfig.GetZigbeeRole.Rsp( - TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetZigbeeRole.Req(TSN=1), responses=[c.NcpConfig.GetZigbeeRole.Rsp( - TSN=1, StatusCat=t.StatusCategory(4), StatusCode=0, + TSN=1, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1))] ) zboss_server.reply_once_to( request=c.NcpConfig.GetJoinStatus.Req(TSN=2), responses=[c.NcpConfig.GetJoinStatus.Rsp( - TSN=2, StatusCat=t.StatusCategory(4), StatusCode=1, + TSN=2, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK, Joined=0)] ) @@ -249,7 +291,9 @@ async def test_not_configured(make_application): TSN=3, Option=t.ResetOptions(0) ), responses=[c.NcpConfig.NCPModuleReset.Rsp( - TSN=3, StatusCat=t.StatusCategory(4), StatusCode=1 + TSN=3, + StatusCat=t.StatusCategory(4), + StatusCode=t.StatusCodeGeneric.OK )] ) diff --git a/tests/application/test_zdo_requests.py b/tests/application/test_zdo_requests.py index 15f5b62..fec1c09 100644 --- a/tests/application/test_zdo_requests.py +++ b/tests/application/test_zdo_requests.py @@ -43,7 +43,7 @@ async def update_channel(req): responses=[c.APS.DataReq.Rsp( TSN=123, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=t.EUI64.convert("00:00:00:00:00:00:aa:bb"), DstEndpoint=1, SrcEndpoint=1, @@ -64,7 +64,7 @@ async def update_channel(req): c.ZDO.MgmtNwkUpdate.Rsp( TSN=123, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.OK, ScannedChannels=t.Channels.from_channel_list([new_channel]), TotalTransmissions=1, TransmissionFailures=0, diff --git a/tests/conftest.py b/tests/conftest.py index e71f658..6e3a650 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -515,7 +515,7 @@ def get_join_status(self, request): return c.NcpConfig.GetJoinStatus.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, Joined=0x01 # Assume device is joined for this example ) @@ -525,7 +525,7 @@ def get_ncp_reset(self, request): return c.NcpConfig.NCPModuleReset.Rsp( TSN=0xFF, StatusCat=t.StatusCategory(1), - StatusCode=20 + StatusCode=t.StatusCodeGeneric.OK ) @reply_to(c.NcpConfig.GetShortAddr.Req(partial=True)) @@ -534,7 +534,7 @@ def get_short_addr(self, request): return c.NcpConfig.GetShortAddr.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, NWKAddr=t.NWK(0x1234) # Example NWK address ) @@ -544,7 +544,7 @@ def on_zdo_request(self, req): return c.APS.DataReq.Rsp( TSN=req.TSN, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.OK, DstAddr=req.DstAddr, DstEndpoint=req.DstEndpoint, SrcEndpoint=req.SrcEndpoint, @@ -558,7 +558,7 @@ def get_local_ieee(self, request): return c.NcpConfig.GetLocalIEEE.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, MacInterfaceNum=request.MacInterfaceNum, IEEE=t.EUI64([0, 1, 2, 3, 4, 5, 6, 7]) # Example IEEE address ) @@ -569,7 +569,7 @@ def get_zigbee_role(self, request): return c.NcpConfig.GetZigbeeRole.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) # Example role ) @@ -579,7 +579,7 @@ def get_extended_panid(self, request): return c.NcpConfig.GetExtendedPANID.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ExtendedPANID=t.EUI64.convert("00124b0001ab89cd") # Example PAN ID ) @@ -589,7 +589,7 @@ def get_permit_join(self, request): return c.ZDO.PermitJoin.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ) @reply_to(c.NcpConfig.GetShortPANID.Req(partial=True)) @@ -598,7 +598,7 @@ def get_short_panid(self, request): return c.NcpConfig.GetShortPANID.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, PANID=t.PanId(0x5678) # Example short PAN ID ) @@ -613,7 +613,7 @@ def get_current_channel(self, request): return c.NcpConfig.GetCurrentChannel.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, Page=0, Channel=t.Channels(channel) ) @@ -624,7 +624,7 @@ def get_channel_mask(self, request): return c.NcpConfig.GetChannelMask.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ChannelList=t.ChannelEntryList( [t.ChannelEntry(page=1, channel_mask=0x07fff800)]) ) # Example mask @@ -632,9 +632,9 @@ def get_channel_mask(self, request): @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) def read_nvram(self, request): """Handle NVRAM read.""" - status_code = 1 + status_code = t.StatusCodeGeneric.ERROR if request.DatasetId == t.DatasetId.ZB_NVRAM_COMMON_DATA: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSCommonData( byte_count=100, bitfield=1, @@ -664,7 +664,7 @@ def read_nvram(self, request): nvram_version = 3 dataset_version = 1 elif request.DatasetId == t.DatasetId.ZB_IB_COUNTERS: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSIbCounters( byte_count=8, nib_counter=100, # Example counter value @@ -673,7 +673,7 @@ def read_nvram(self, request): nvram_version = 1 dataset_version = 1 elif request.DatasetId == t.DatasetId.ZB_NVRAM_ADDR_MAP: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSNwkAddrMap( header=t.NwkAddrMapHeader( byte_count=100, @@ -702,7 +702,7 @@ def read_nvram(self, request): nvram_version = 2 dataset_version = 1 elif request.DatasetId == t.DatasetId.ZB_NVRAM_APS_SECURE_DATA: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSApsSecureKeys( header=10, items=[ @@ -741,7 +741,7 @@ def get_trust_center_addr(self, request): return c.NcpConfig.GetTrustCenterAddr.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, TCIEEE=t.EUI64.convert("00:11:22:33:44:55:66:77") # Example Trust Center IEEE address ) @@ -752,7 +752,7 @@ def get_rx_on_when_idle(self, request): return c.NcpConfig.GetRxOnWhenIdle.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RxOnWhenIdle=1 # Example RxOnWhenIdle value ) @@ -762,7 +762,7 @@ def start_without_formation(self, request): return c.NWK.StartWithoutFormation.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=0 # Example status code + StatusCode=t.StatusCodeGeneric.OK # Example status code ) @reply_to(c.NcpConfig.GetModuleVersion.Req(partial=True)) @@ -771,7 +771,7 @@ def get_module_version(self, request): return c.NcpConfig.GetModuleVersion.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, # Example status code + StatusCode=t.StatusCodeGeneric.OK, # Example status code FWVersion=1, # Example firmware version StackVersion=2, # Example stack version ProtocolVersion=3 # Example protocol version @@ -783,7 +783,7 @@ def set_simple_desc(self, request): return c.AF.SetSimpleDesc.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20 # Example status code + StatusCode=t.StatusCodeGeneric.OK # Example status code ) @reply_to(c.NcpConfig.GetEDTimeout.Req(partial=True)) @@ -792,7 +792,7 @@ def get_ed_timeout(self, request): return c.NcpConfig.GetEDTimeout.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, Timeout=t.TimeoutIndex(0x01) # Example timeout value ) @@ -802,7 +802,7 @@ def get_max_children(self, request): return c.NcpConfig.GetMaxChildren.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, ChildrenNbr=5 # Example max children ) @@ -812,7 +812,7 @@ def get_authentication_status(self, request): return c.NcpConfig.GetAuthenticationStatus.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, Authenticated=1 # Example authenticated value ) @@ -822,7 +822,7 @@ def get_parent_addr(self, request): return c.NcpConfig.GetParentAddr.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, NWKParentAddr=t.NWK(0x1234) # Example parent NWK address ) @@ -832,7 +832,7 @@ def get_coordinator_version(self, request): return c.NcpConfig.GetCoordinatorVersion.Rsp( TSN=request.TSN, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, CoordinatorVersion=1 # Example coordinator version ) diff --git a/tests/test_commands.py b/tests/test_commands.py index 3a70a2b..a0d26f4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -84,19 +84,21 @@ def test_command_param_binding(): with pytest.raises(ValueError): c.NcpConfig.GetModuleVersion.Rsp(TSN="invalid", StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=123456, StackVersion=789012, ProtocolVersion=345678 ) # Example for correct command invocation - valid_rsp = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory(1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678) + valid_rsp = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) assert isinstance(valid_rsp.FWVersion, t.uint32_t) assert isinstance(valid_rsp.StackVersion, t.uint32_t) assert isinstance(valid_rsp.ProtocolVersion, t.uint32_t) @@ -105,7 +107,8 @@ def test_command_param_binding(): with pytest.raises(ValueError): c.NcpConfig.GetModuleVersion.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, FWVersion=10 ** 20, + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=10 ** 20, StackVersion=789012, ProtocolVersion=345678) @@ -129,10 +132,12 @@ def test_command_param_binding(): ) # Parameters can be looked up by name - zigbee_role = c.NcpConfig.GetZigbeeRole.Rsp(TSN=10, - StatusCat=t.StatusCategory(1), - StatusCode=20, - DeviceRole=t.DeviceRole.ZC) + zigbee_role = c.NcpConfig.GetZigbeeRole.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + DeviceRole=t.DeviceRole.ZC + ) assert zigbee_role.DeviceRole == t.DeviceRole.ZC # Invalid ones cannot @@ -146,7 +151,7 @@ def test_command_optional_params(): basic_ieee_addr_rsp = c.ZDO.IeeeAddrReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), RemoteDevNWK=t.NWK(0x1234) ) @@ -155,7 +160,7 @@ def test_command_optional_params(): full_ieee_addr_rsp = c.ZDO.IeeeAddrReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), RemoteDevNWK=t.NWK(0x1234), NumAssocDev=5, @@ -191,7 +196,7 @@ def test_command_optional_params_failures(): c.ZDO.IeeeAddrReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), RemoteDevNWK=t.NWK(0x1234), NumAssocDev=5, @@ -203,7 +208,7 @@ def test_command_optional_params_failures(): partial = c.ZDO.IeeeAddrReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RemoteDevIEEE=t.EUI64([00, 11, 22, 33, 44, 55, 66, 77]), RemoteDevNWK=t.NWK(0x1234), NumAssocDev=5, @@ -236,7 +241,7 @@ def test_simple_descriptor(): c1 = c.ZDO.SimpleDescriptorReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, SimpleDesc=simple_descriptor, NwkAddr=t.NWK(0x1234) ) @@ -256,7 +261,7 @@ def test_simple_descriptor(): c2 = c.ZDO.SimpleDescriptorReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, SimpleDesc=sp_simple_descriptor, NwkAddr=t.NWK(0x1234) ) @@ -278,7 +283,7 @@ def test_command_immutability(): command1 = c.ZDO.IeeeAddrReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RemoteDevNWK=t.NWK(0x1234), NumAssocDev=5, StartIndex=0, @@ -288,7 +293,7 @@ def test_command_immutability(): command2 = c.ZDO.IeeeAddrReq.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, RemoteDevNWK=t.NWK(0x1234), NumAssocDev=5, StartIndex=0, @@ -305,7 +310,7 @@ def test_command_immutability(): command1.partial = False with pytest.raises(RuntimeError): - command1.StatusCode = 20 + command1.StatusCode = t.StatusCodeGeneric.OK with pytest.raises(RuntimeError): command1.NumAssocDev = 5 @@ -318,12 +323,14 @@ def test_command_immutability(): def test_command_serialization(): """Test command serialization.""" - command = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory(1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678) + command = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) frame = command.to_frame() assert frame.hl_packet.data == bytes.fromhex( @@ -332,52 +339,60 @@ def test_command_serialization(): # Partial frames cannot be serialized with pytest.raises(ValueError): - partial1 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory( - 1), - StatusCode=20, - FWVersion=123456, - # StackVersion=789012, - ProtocolVersion=345678, - partial=True) + partial1 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + # StackVersion=789012, + ProtocolVersion=345678, + partial=True + ) partial1.to_frame() # Partial frames cannot be serialized, even if all params are filled out with pytest.raises(ValueError): - partial2 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory( - 1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678, - partial=True) + partial2 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678, + partial=True + ) partial2.to_frame() def test_command_equality(): """Test command equality.""" - command1 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory(1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678) - - command2 = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory(1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678) - - command3 = c.NcpConfig.GetModuleVersion.Rsp(TSN=20, - StatusCat=t.StatusCategory(1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678) + command1 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) + + command2 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) + + command3 = c.NcpConfig.GetModuleVersion.Rsp( + TSN=20, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) assert command1 == command1 assert command1.matches(command1) @@ -394,13 +409,16 @@ def test_command_equality(): assert not command1.matches( c.NcpConfig.GetModuleVersion.Rsp( - TSN=10, StatusCat=t.StatusCategory(1), StatusCode=20, partial=True + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + partial=True ) ) assert c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, partial=True ).matches(command1) @@ -408,21 +426,21 @@ def test_command_equality(): assert c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, StackVersion=None, partial=True ).matches(command1) assert c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, StackVersion=789012, partial=True ).matches(command1) assert not c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, StackVersion=79000, partial=True ).matches(command1) @@ -430,19 +448,21 @@ def test_command_equality(): # Different frame types do not match, even if they have the same structure assert not c.ZDO.MgtLeave.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20).matches( + StatusCode=t.StatusCodeGeneric.OK).matches( c.ZDO.PermitJoin.Rsp(partial=True) ) def test_command_deserialization(): """Test command deserialization.""" - command = c.NcpConfig.GetModuleVersion.Rsp(TSN=10, - StatusCat=t.StatusCategory(1), - StatusCode=20, - FWVersion=123456, - StackVersion=789012, - ProtocolVersion=345678) + command = c.NcpConfig.GetModuleVersion.Rsp( + TSN=10, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, + FWVersion=123456, + StackVersion=789012, + ProtocolVersion=345678 + ) assert type(command).from_frame(command.to_frame()) == command assert ( @@ -466,8 +486,8 @@ def test_command_deserialization(): with pytest.raises(ValueError): c.ZDO.MgtLeave.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20).from_frame( + StatusCode=t.StatusCodeGeneric.OK).from_frame( c.ZDO.PermitJoin.Rsp(TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20).to_frame() + StatusCode=t.StatusCodeGeneric.OK).to_frame() ) diff --git a/tests/test_uart.py b/tests/test_uart.py index 0bb394a..f8574a9 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -75,7 +75,7 @@ def test_uart_rx_basic(connected_uart): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() @@ -104,7 +104,7 @@ def test_uart_rx_byte_by_byte(connected_uart): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() @@ -126,7 +126,7 @@ def test_uart_rx_byte_by_byte_garbage(connected_uart): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() @@ -158,7 +158,7 @@ def test_uart_rx_big_garbage(connected_uart): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() @@ -189,7 +189,7 @@ def test_uart_rx_corrupted_fcs(connected_uart): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() @@ -212,7 +212,7 @@ def test_uart_rx_sof_stress(connected_uart): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() @@ -242,7 +242,7 @@ def test_uart_frame_received_error(connected_uart, mocker): test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ) test_frame = test_command.to_frame() diff --git a/tests/test_utils.py b/tests/test_utils.py index ff4b775..daade53 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -23,7 +23,7 @@ def test_command_deduplication_complex(): c.NcpConfig.GetModuleVersion.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, FWVersion=1, StackVersion=2, ProtocolVersion=3, @@ -35,27 +35,27 @@ def test_command_deduplication_complex(): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetZigbeeRole.Rsp( TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=10, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(2) ), c.NcpConfig.GetNwkKeys.Rsp( partial=True, TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=10, + StatusCode=t.StatusCodeGeneric.OK, KeyNumber1=10, ), c.NcpConfig.GetNwkKeys.Rsp( partial=True, TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=10, + StatusCode=t.StatusCodeGeneric.OK, KeyNumber1=10, KeyNumber2=20, ), @@ -63,7 +63,7 @@ def test_command_deduplication_complex(): partial=True, TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=10, + StatusCode=t.StatusCodeGeneric.OK, KeyNumber1=10, KeyNumber2=20, KeyNumber3=30, @@ -82,20 +82,20 @@ def test_command_deduplication_complex(): c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=20, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(1) ), c.NcpConfig.GetZigbeeRole.Rsp( TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=10, + StatusCode=t.StatusCodeGeneric.OK, DeviceRole=t.DeviceRole(2) ), c.NcpConfig.GetNwkKeys.Rsp( partial=True, TSN=11, StatusCat=t.StatusCategory(2), - StatusCode=10, + StatusCode=t.StatusCodeGeneric.OK, KeyNumber1=10, ), c.NcpConfig.GetNwkKeys.Rsp( diff --git a/zigpy_zboss/types/commands.py b/zigpy_zboss/types/commands.py index 7c7133d..8ec8b9a 100644 --- a/zigpy_zboss/types/commands.py +++ b/zigpy_zboss/types/commands.py @@ -721,7 +721,8 @@ class Relationship(t.enum8): STATUS_SCHEMA = ( - t.Param("TSN", t.uint8_t, "Transmit Sequence Number"), - t.Param("StatusCat", StatusCategory, "Status category code"), - t.Param("StatusCode", StatusCodeGeneric, "Status code inside category"), + t_named.Param("TSN", t.uint8_t, "Transmit Sequence Number"), + t_named.Param("StatusCat", StatusCategory, "Status category code"), + t_named.Param( + "StatusCode", StatusCodeGeneric, "Status code inside category"), ) From 6d0791bd813c057d2e169cef02a4b04770cce985 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:21:30 +0400 Subject: [PATCH 49/57] remove api.connection_made call --- zigpy_zboss/uart.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zigpy_zboss/uart.py b/zigpy_zboss/uart.py index e204f85..af4e39d 100644 --- a/zigpy_zboss/uart.py +++ b/zigpy_zboss/uart.py @@ -81,9 +81,6 @@ def connection_made( SERIAL_LOGGER.info(message) self._connected_event.set() - if self._api is not None: - self._api.connection_made() - def connection_lost(self, exc: typing.Optional[Exception]) -> None: """Lost connection.""" LOGGER.debug("Connection has been lost: %r", exc) From 60151418ba39776fe5263c20269cdb5229bba617 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:21:54 +0400 Subject: [PATCH 50/57] clear api listeners at zboss.close --- zigpy_zboss/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index fa1a6b5..ae3b091 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -49,8 +49,6 @@ def __init__(self, config: conf.ConfigType): self._listeners = defaultdict(list) self._blocking_request_lock = asyncio.Lock() - self.capabilities = None - self.nvram = NVRAMHelper(self) self.network_info: zigpy.state.NetworkInformation = None self.node_info: zigpy.state.NodeInfo = None @@ -118,12 +116,18 @@ def close(self) -> None: self._app = None self.version = None + for _, listeners in self._listeners.items(): + for listener in listeners: + listener.cancel() + self._listeners.clear() + if self._uart is not None: self._uart.close() self._uart = None def frame_received(self, frame: Frame) -> bool: """Frame has been received. + Called when a frame has been received. Returns whether or not the frame was handled by any listener. From 31b6eef989b0749c66975d7b348963363d0b9c75 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:00:48 +0400 Subject: [PATCH 51/57] update --- tests/api/test_connect.py | 13 +++++++++---- tests/api/test_response.py | 2 +- tests/test_commands.py | 2 +- zigpy_zboss/api.py | 5 +---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/api/test_connect.py b/tests/api/test_connect.py index 4bbfe59..dcb981f 100644 --- a/tests/api/test_connect.py +++ b/tests/api/test_connect.py @@ -27,8 +27,7 @@ async def test_api_close(connected_zboss, mocker): uart = zboss._uart mocker.spy(uart, "close") - # add some dummy fields and listeners, should be cleared on close - zboss.capabilities = 4 + # add some dummy listeners, should be cleared on close zboss._listeners = { 'listener1': [mocker.Mock()], 'listener2': [mocker.Mock()] } @@ -38,7 +37,7 @@ async def test_api_close(connected_zboss, mocker): # Make sure our UART was actually closed assert zboss._uart is None assert zboss._app is None - assert uart.close.call_count == 2 + assert uart.close.call_count == 1 # ZBOSS.close should not throw any errors if called multiple times zboss.close() @@ -47,7 +46,13 @@ async def test_api_close(connected_zboss, mocker): def dict_minus(d, minus): return {k: v for k, v in d.items() if k not in minus} - ignored_keys = ["_blocking_request_lock", "nvram", "version"] + ignored_keys = [ + "_blocking_request_lock", + "_reset_uart_reconnect", + "_disconnected_event", + "nvram", + "version" + ] # Closing ZBOSS should reset it completely to that of a fresh object # We have to ignore our mocked method and the lock diff --git a/tests/api/test_response.py b/tests/api/test_response.py index 8f76ac2..c810f53 100644 --- a/tests/api/test_response.py +++ b/tests/api/test_response.py @@ -258,7 +258,7 @@ async def test_response_callback_simple(connected_zboss, event_loop, mocker): bad_response = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, StatusCat=t.StatusCategory(1), - StatusCode=0, + StatusCode=t.StatusCodeGeneric.ERROR, DeviceRole=t.DeviceRole(1) ) diff --git a/tests/test_commands.py b/tests/test_commands.py index a0d26f4..8939974 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -334,7 +334,7 @@ def test_command_serialization(): frame = command.to_frame() assert frame.hl_packet.data == bytes.fromhex( - "0A011440E20100140A0C004E460500" + "0A010040E20100140A0C004E460500" ) # Partial frames cannot be serialized diff --git a/zigpy_zboss/api.py b/zigpy_zboss/api.py index ae3b091..c01ab15 100644 --- a/zigpy_zboss/api.py +++ b/zigpy_zboss/api.py @@ -322,10 +322,7 @@ def register_indication_listener( async def version(self): """Get NCP module version.""" - if self._app is not None: - tsn = self._app.get_sequence() - else: - tsn = 0 + tsn = self._app.get_sequence() if self._app is not None else 0 req = c.NcpConfig.GetModuleVersion.Req(TSN=tsn) res = await self.request(req) if res.StatusCode: From 61e05967fc5c4e0c8876faa3c59a27dc9478e368 Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:25:40 +0400 Subject: [PATCH 52/57] update --- tests/test_types_named.py | 8 +---- tests/test_uart.py | 69 ++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/tests/test_types_named.py b/tests/test_types_named.py index 887386a..d0e83d5 100644 --- a/tests/test_types_named.py +++ b/tests/test_types_named.py @@ -1,6 +1,4 @@ """Test named types.""" -import pytest - import zigpy_zboss.types as t @@ -32,9 +30,5 @@ def test_channel_entry(): # Test __repr__ expected_repr = \ - "ChannelEntry(page=1, channels=)" + "ChannelEntry(page=1, channel_mask=)" assert repr(channel_entry) == expected_repr - - # Test handling of None types for page or channel_mask - with pytest.raises(AttributeError): - t.ChannelEntry(page=None, channel_mask=None) diff --git a/tests/test_uart.py b/tests/test_uart.py index f8574a9..e151774 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -5,7 +5,7 @@ import zigpy_zboss.commands as c import zigpy_zboss.config as conf import zigpy_zboss.types as t -from zigpy_zboss import uart as znp_uart +from zigpy_zboss import uart as zboss_uart from zigpy_zboss.checksum import CRC8 from zigpy_zboss.frames import Frame @@ -15,11 +15,11 @@ def connected_uart(mocker): """Uart connected fixture.""" zboss = mocker.Mock() config = { - conf.CONF_DEVICE_PATH: "/dev/ttyACM0", + conf.CONF_DEVICE_PATH: "/dev/ttyFAKE0", conf.CONF_DEVICE_BAUDRATE: 115200, conf.CONF_DEVICE_FLOW_CONTROL: None} - uart = znp_uart.ZbossNcpProtocol(config, zboss) + uart = zboss_uart.ZbossNcpProtocol(config, zboss) uart.connection_made(mocker.Mock()) yield zboss, uart @@ -70,7 +70,7 @@ def create_serial_conn(loop, protocol_factory, url, *args, **kwargs): def test_uart_rx_basic(connected_uart): """Test UART basic receive.""" - znp, uart = connected_uart + zboss, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -86,12 +86,12 @@ def test_uart_rx_basic(connected_uart): uart.data_received(test_frame_bytes) - znp.frame_received.assert_called_once_with(test_frame) + zboss.frame_received.assert_called_once_with(test_frame) def test_uart_str_repr(connected_uart): """Test uart representation.""" - znp, uart = connected_uart + zboss, uart = connected_uart str(uart) repr(uart) @@ -99,7 +99,7 @@ def test_uart_str_repr(connected_uart): def test_uart_rx_byte_by_byte(connected_uart): """Test uart RX byte by byte.""" - znp, uart = connected_uart + zboss, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -116,12 +116,12 @@ def test_uart_rx_byte_by_byte(connected_uart): for byte in test_frame_bytes: uart.data_received(bytes([byte])) - znp.frame_received.assert_called_once_with(test_frame) + zboss.frame_received.assert_called_once_with(test_frame) def test_uart_rx_byte_by_byte_garbage(connected_uart): """Test uart RX byte by byte garbage.""" - znp, uart = connected_uart + zboss, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -148,12 +148,12 @@ def test_uart_rx_byte_by_byte_garbage(connected_uart): for byte in data: uart.data_received(bytes([byte])) - znp.frame_received.assert_called_once_with(test_frame) + zboss.frame_received.assert_called_once_with(test_frame) def test_uart_rx_big_garbage(connected_uart): """Test uart RX big garbage.""" - znp, uart = connected_uart + zboss, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -179,12 +179,12 @@ def test_uart_rx_big_garbage(connected_uart): # The frame should be parsed identically regardless of framing uart.data_received(data) - znp.frame_received.assert_called_once_with(test_frame) + zboss.frame_received.assert_called_once_with(test_frame) def test_uart_rx_corrupted_fcs(connected_uart): """Test uart RX corrupted.""" - znp, uart = connected_uart + zboss, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -202,12 +202,12 @@ def test_uart_rx_corrupted_fcs(connected_uart): uart.data_received(test_frame_bytes[:-1]) uart.data_received(b"\x00") - assert not znp.frame_received.called + assert not zboss.frame_received.called def test_uart_rx_sof_stress(connected_uart): """Test uart RX signature stress.""" - znp, uart = connected_uart + zboss, uart = connected_uart test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -231,13 +231,13 @@ def test_uart_rx_sof_stress(connected_uart): ) # We should see the valid frame exactly once - znp.frame_received.assert_called_once_with(test_frame) + zboss.frame_received.assert_called_once_with(test_frame) def test_uart_frame_received_error(connected_uart, mocker): """Test uart frame received error.""" - znp, uart = connected_uart - znp.frame_received = mocker.Mock(side_effect=RuntimeError("An error")) + zboss, uart = connected_uart + zboss.frame_received = mocker.Mock(side_effect=RuntimeError("An error")) test_command = c.NcpConfig.GetZigbeeRole.Rsp( TSN=10, @@ -251,12 +251,12 @@ def test_uart_frame_received_error(connected_uart, mocker): test_frame.ll_header, test_frame.hl_packet ).serialize() - # Errors thrown by znp.frame_received should + # Errors thrown by zboss.frame_received should # not impact how many frames are handled uart.data_received(test_frame_bytes * 3) # We should have received all three frames - assert znp.frame_received.call_count == 3 + assert zboss.frame_received.call_count == 3 @pytest.mark.asyncio @@ -264,29 +264,30 @@ async def test_connection_lost(dummy_serial_conn, mocker, event_loop): """Test connection lost.""" device, _ = dummy_serial_conn - znp = mocker.Mock() + zboss = mocker.Mock() conn_lost_fut = event_loop.create_future() - znp.connection_lost = conn_lost_fut.set_result + zboss.connection_lost = conn_lost_fut.set_result - protocol = await znp_uart.connect( - conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=znp + protocol = await zboss_uart.connect( + conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=zboss ) exception = RuntimeError("Uh oh, something broke") protocol.connection_lost(exception) - # Losing a connection propagates up to the ZNP object + # Losing a connection propagates up to the ZBOSS object assert (await conn_lost_fut) == exception -@pytest.mark.asyncio -async def test_connection_made(dummy_serial_conn, mocker): - """Test connection made.""" - device, _ = dummy_serial_conn - znp = mocker.Mock() +# ToFix: this is not testing the uart test_connection_made method +# @pytest.mark.asyncio +# async def test_connection_made(dummy_serial_conn, mocker): +# """Test connection made.""" +# device, _ = dummy_serial_conn +# zboss = mocker.Mock() - await znp_uart.connect( - conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=znp - ) +# await zboss_uart.connect( +# conf.SCHEMA_DEVICE({conf.CONF_DEVICE_PATH: device}), api=zboss +# ) - znp.connection_made.assert_called_once_with() +# zboss._uart.connection_made.assert_called_once_with() From 04a876f25e05dd97912ec6ff6888d7c1b3dfdb5c Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:24:11 +0400 Subject: [PATCH 53/57] update --- tests/application/test_startup.py | 64 ++++++++++++++++++------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index a22ce20..af885f2 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -81,36 +81,48 @@ async def test_info(make_application, caplog): ) zboss_server.reply_once_to( - request=c.NcpConfig.GetExtendedPANID.Req(TSN=6), - responses=[c.NcpConfig.GetExtendedPANID.Rsp( + request=c.NcpConfig.GetModuleVersion.Req(TSN=6), + responses=[c.NcpConfig.GetModuleVersion.Rsp( TSN=6, + StatusCat=t.StatusCategory(1), + StatusCode=t.StatusCodeGeneric.OK, # Example status code + FWVersion=1, # Example firmware version + StackVersion=2, # Example stack version + ProtocolVersion=3 # Example protocol version + )] + ) + + zboss_server.reply_once_to( + request=c.NcpConfig.GetExtendedPANID.Req(TSN=7), + responses=[c.NcpConfig.GetExtendedPANID.Rsp( + TSN=7, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, ExtendedPANID=ext_pan_id)] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetShortPANID.Req(TSN=7), + request=c.NcpConfig.GetShortPANID.Req(TSN=8), responses=[c.NcpConfig.GetShortPANID.Rsp( - TSN=7, + TSN=8, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, PANID=t.PanId(pan_id))] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetCurrentChannel.Req(TSN=8), + request=c.NcpConfig.GetCurrentChannel.Req(TSN=9), responses=[c.NcpConfig.GetCurrentChannel.Rsp( - TSN=8, + TSN=9, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, Channel=channel, Page=0)] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetChannelMask.Req(TSN=9), + request=c.NcpConfig.GetChannelMask.Req(TSN=10), responses=[c.NcpConfig.GetChannelMask.Rsp( - TSN=9, + TSN=10, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, ChannelList=[t.ChannelEntry(page=1, channel_mask=channel_mask)])] @@ -118,10 +130,10 @@ async def test_info(make_application, caplog): zboss_server.reply_once_to( request=c.NcpConfig.GetTrustCenterAddr.Req( - TSN=12 + TSN=13 ), responses=[c.NcpConfig.GetTrustCenterAddr.Rsp( - TSN=12, + TSN=13, StatusCat=t.StatusCategory(1), StatusCode=t.StatusCodeGeneric.OK, TCIEEE=t.EUI64.convert("00:11:22:33:44:55:66:77") @@ -130,54 +142,54 @@ async def test_info(make_application, caplog): ) zboss_server.reply_once_to( - request=c.NcpConfig.GetRxOnWhenIdle.Req(TSN=13), + request=c.NcpConfig.GetRxOnWhenIdle.Req(TSN=14), responses=[c.NcpConfig.GetRxOnWhenIdle.Rsp( - TSN=13, + TSN=14, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, RxOnWhenIdle=1)] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetEDTimeout.Req(TSN=14), + request=c.NcpConfig.GetEDTimeout.Req(TSN=15), responses=[c.NcpConfig.GetEDTimeout.Rsp( - TSN=14, + TSN=15, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, Timeout=t.TimeoutIndex(0x00))] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetMaxChildren.Req(TSN=15), + request=c.NcpConfig.GetMaxChildren.Req(TSN=16), responses=[c.NcpConfig.GetMaxChildren.Rsp( - TSN=15, + TSN=16, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, ChildrenNbr=10)] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetAuthenticationStatus.Req(TSN=16), + request=c.NcpConfig.GetAuthenticationStatus.Req(TSN=17), responses=[c.NcpConfig.GetAuthenticationStatus.Rsp( - TSN=16, + TSN=17, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, Authenticated=True)] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetParentAddr.Req(TSN=17), + request=c.NcpConfig.GetParentAddr.Req(TSN=18), responses=[c.NcpConfig.GetParentAddr.Rsp( - TSN=17, + TSN=18, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, NWKParentAddr=parent_address)] ) zboss_server.reply_once_to( - request=c.NcpConfig.GetCoordinatorVersion.Req(TSN=18), + request=c.NcpConfig.GetCoordinatorVersion.Req(TSN=19), responses=[c.NcpConfig.GetCoordinatorVersion.Rsp( - TSN=18, + TSN=19, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, CoordinatorVersion=coordinator_version)] @@ -185,13 +197,13 @@ async def test_info(make_application, caplog): zboss_server.reply_once_to( request=c.ZDO.PermitJoin.Req( - TSN=20, + TSN=21, DestNWK=t.NWK(0x0000), PermitDuration=t.uint8_t(0), TCSignificance=t.uint8_t(0x01), ), responses=[c.ZDO.PermitJoin.Rsp( - TSN=20, + TSN=21, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK, )] @@ -199,10 +211,10 @@ async def test_info(make_application, caplog): zboss_server.reply_once_to( request=c.NcpConfig.NCPModuleReset.Req( - TSN=21, Option=t.ResetOptions(0) + TSN=22, Option=t.ResetOptions(0) ), responses=[c.NcpConfig.NCPModuleReset.Rsp( - TSN=21, + TSN=22, StatusCat=t.StatusCategory(4), StatusCode=t.StatusCodeGeneric.OK )] From 85daab9a7d8e483281784293397cb745e145fa1f Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:42:28 +0400 Subject: [PATCH 54/57] fix test_startup --- tests/application/test_startup.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index af885f2..451611e 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -228,15 +228,10 @@ async def test_info(make_application, caplog): assert app.state.network_info.channel == channel assert app.state.network_info.channel_mask == channel_mask assert app.state.network_info.network_key.seq == 1 - assert app.state.network_info.stack_specific[ - "parent_nwk" - ] == parent_address - assert app.state.network_info.stack_specific[ - "authenticated" - ] == 1 - assert app.state.network_info.stack_specific[ - "coordinator_version" - ] == coordinator_version + zboss_stack_specific = app.state.network_info.stack_specific["zboss"] + assert zboss_stack_specific["parent_nwk"] == parent_address + assert zboss_stack_specific["authenticated"] == 1 + assert zboss_stack_specific["coordinator_version"] == coordinator_version # Anything to make sure it's set assert app._device.node_desc.maximum_outgoing_transfer_size == 82 From ad069c4b14a0129e4d70b3967c144d777aa68b9a Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:42:52 +0400 Subject: [PATCH 55/57] missing part --- tests/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6e3a650..8171b59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -907,9 +907,9 @@ def connection_lost(self, exc): @reply_to(c.NcpConfig.ReadNVRAM.Req(partial=True)) def read_nvram(self, request): """Handle NVRAM read.""" - status_code = 1 + status_code = t.StatusCodeGeneric.ERROR if request.DatasetId == t.DatasetId.ZB_NVRAM_COMMON_DATA: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSCommonData( byte_count=100, bitfield=1, @@ -939,7 +939,7 @@ def read_nvram(self, request): nvram_version = 3 dataset_version = 1 elif request.DatasetId == t.DatasetId.ZB_IB_COUNTERS: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSIbCounters( byte_count=8, nib_counter=100, # Example counter value @@ -948,7 +948,7 @@ def read_nvram(self, request): nvram_version = 1 dataset_version = 1 elif request.DatasetId == t.DatasetId.ZB_NVRAM_ADDR_MAP: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSNwkAddrMap( header=t.NwkAddrMapHeader( byte_count=100, @@ -977,7 +977,7 @@ def read_nvram(self, request): nvram_version = 2 dataset_version = 1 elif request.DatasetId == t.DatasetId.ZB_NVRAM_APS_SECURE_DATA: - status_code = 0 + status_code = t.StatusCodeGeneric.OK dataset = t.DSApsSecureKeys( header=10, items=[ From 13841124ec58ef36f55c5fa58bb1445006b8511e Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:44:19 +0400 Subject: [PATCH 56/57] remove PR to trigger CI --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fb3a22..2d34021 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,6 @@ name: CI # yamllint disable-line rule:truthy on: push: - pull_request: ~ env: CODE_FOLDER: zigpy_zboss From aa69ef2e2b5ffec54204337047920ba4887e17ec Mon Sep 17 00:00:00 2001 From: DamKas <48238600+DamKast@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:35:23 +0400 Subject: [PATCH 57/57] fix test_reuests --- tests/application/test_requests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/application/test_requests.py b/tests/application/test_requests.py index 5cd58a8..eb80a52 100644 --- a/tests/application/test_requests.py +++ b/tests/application/test_requests.py @@ -239,6 +239,8 @@ async def on_request_sent(): await group.endpoint.on_off.on() request_sent.set_result(True) + await asyncio.sleep(0.01) + await app.shutdown()